local Root = script:FindFirstAncestor("Signals")
local Packages = Root.Parent

local JestGlobals = require(Packages.Dev.JestGlobals)
local expect = JestGlobals.expect
local it = JestGlobals.it

local SignalsScheduler = require(Packages.SignalsScheduler)
local batch = SignalsScheduler.batch

local Signals = require(script.Parent.Parent.Signals)
local createSignal = Signals.createSignal
local createComputed = Signals.createComputed
local createEffect = Signals.createEffect

local RUNS = 10

it("should execute an effect only once with no dependencies", function()
	local count = 0

	local dispose = createEffect(function(_scope)
		count += 1
	end)

	task.wait()
	expect(count).toEqual(1)
	task.wait()
	expect(count).toEqual(1)
	task.wait()
	expect(count).toEqual(1)

	dispose()

	task.wait()
	expect(count).toEqual(1)
	task.wait()
	expect(count).toEqual(1)
	task.wait()
	expect(count).toEqual(1)
end)

it("should be fine to call dispose multiple times", function()
	local count = 0

	local dispose = createEffect(function(_scope)
		count += 1
	end)

	task.wait()
	expect(count).toEqual(1)
	task.wait()
	expect(count).toEqual(1)
	task.wait()
	expect(count).toEqual(1)

	dispose()

	task.wait()
	expect(count).toEqual(1)
	task.wait()
	expect(count).toEqual(1)
	task.wait()
	expect(count).toEqual(1)

	dispose()

	task.wait()
	expect(count).toEqual(1)
	task.wait()
	expect(count).toEqual(1)
	task.wait()
	expect(count).toEqual(1)
end)

it("should execute an effect when a dependency changes", function()
	local count = 0
	local get, set = createSignal(0)

	local dispose = createEffect(function(scope)
		get(scope)
		count += 1
	end) -- effect should fire once

	task.wait()
	expect(count).toEqual(1)
	task.wait()
	expect(count).toEqual(1)
	task.wait()
	expect(count).toEqual(1)

	set(1) -- effect should fire again

	task.wait()
	expect(count).toEqual(2)
	task.wait()
	expect(count).toEqual(2)
	task.wait()
	expect(count).toEqual(2)

	set(2) -- effect should fire once more

	task.wait()
	expect(count).toEqual(3)
	task.wait()
	expect(count).toEqual(3)
	task.wait()
	expect(count).toEqual(3)

	set(2) -- effect should not fire if source did not change

	task.wait()
	expect(count).toEqual(3)
	task.wait()
	expect(count).toEqual(3)
	task.wait()
	expect(count).toEqual(3)

	dispose() -- dispose should not change value

	task.wait()
	expect(count).toEqual(3)
	task.wait()
	expect(count).toEqual(3)
	task.wait()
	expect(count).toEqual(3)

	set(3) -- should be a no-op after being disposed

	task.wait()
	expect(count).toEqual(3)
	task.wait()
	expect(count).toEqual(3)
	task.wait()
	expect(count).toEqual(3)
end)

it("should support dynamic/changing dependencies", function()
	local countA, countB = 0, 0
	local getToggle, setToggle = createSignal(true)
	local getA, setA = createSignal(0)
	local getB, setB = createSignal(0)

	local dispose = createEffect(function(scope)
		if getToggle(scope) then
			countA += 1
			getA(scope)
		else
			countB += 1
			getB(scope)
		end
	end) -- effect should fire once

	task.wait()
	expect(countA).toEqual(1)
	expect(countB).toEqual(0)
	task.wait()
	expect(countA).toEqual(1)
	expect(countB).toEqual(0)
	task.wait()
	expect(countA).toEqual(1)
	expect(countB).toEqual(0)

	setA(1) -- should re-run effect

	task.wait()
	expect(countA).toEqual(2)
	expect(countB).toEqual(0)
	task.wait()
	expect(countA).toEqual(2)
	expect(countB).toEqual(0)
	task.wait()
	expect(countA).toEqual(2)
	expect(countB).toEqual(0)

	setB(1) -- should not re-run effect

	task.wait()
	expect(countA).toEqual(2)
	expect(countB).toEqual(0)
	task.wait()
	expect(countA).toEqual(2)
	expect(countB).toEqual(0)
	task.wait()
	expect(countA).toEqual(2)
	expect(countB).toEqual(0)

	setToggle(false) -- should re-run effect

	task.wait()
	expect(countA).toEqual(2)
	expect(countB).toEqual(1)
	task.wait()
	expect(countA).toEqual(2)
	expect(countB).toEqual(1)
	task.wait()
	expect(countA).toEqual(2)
	expect(countB).toEqual(1)

	setA(2) -- should not re-run effect

	task.wait()
	expect(countA).toEqual(2)
	expect(countB).toEqual(1)
	task.wait()
	expect(countA).toEqual(2)
	expect(countB).toEqual(1)
	task.wait()
	expect(countA).toEqual(2)
	expect(countB).toEqual(1)

	setB(2) -- should re-run effect

	task.wait()
	expect(countA).toEqual(2)
	expect(countB).toEqual(2)
	task.wait()
	expect(countA).toEqual(2)
	expect(countB).toEqual(2)
	task.wait()
	expect(countA).toEqual(2)
	expect(countB).toEqual(2)

	dispose() -- should not re-run effect

	task.wait()
	expect(countA).toEqual(2)
	expect(countB).toEqual(2)
	task.wait()
	expect(countA).toEqual(2)
	expect(countB).toEqual(2)
	task.wait()
	expect(countA).toEqual(2)
	expect(countB).toEqual(2)

	setA(3) -- should not re-run effect after being disposed

	task.wait()
	expect(countA).toEqual(2)
	expect(countB).toEqual(2)
	task.wait()
	expect(countA).toEqual(2)
	expect(countB).toEqual(2)
	task.wait()
	expect(countA).toEqual(2)
	expect(countB).toEqual(2)

	setB(3) -- should not re-run effect after being disposed

	task.wait()
	expect(countA).toEqual(2)
	expect(countB).toEqual(2)
	task.wait()
	expect(countA).toEqual(2)
	expect(countB).toEqual(2)
	task.wait()
	expect(countA).toEqual(2)
	expect(countB).toEqual(2)
end)

it("should execute effects once per state update for simple graphs", function()
	local sum = 0

	local getA, setA = createSignal(1)
	local getB, setB = createSignal(2)
	local getC, setC = createSignal(3)

	for i = 1, RUNS do
		local function getPadding()
			return 39 * (i - 1)
		end

		task.wait()
		expect(sum).toEqual(0 + getPadding())

		local dispose1 = createEffect(function(scope)
			sum = sum + getA(scope)
		end)
		local dispose2 = createEffect(function(scope)
			sum = sum + getA(scope)
		end)
		local dispose3 = createEffect(function(scope)
			sum = sum + getA(scope)
		end)

		task.wait()
		expect(sum).toEqual(3 + getPadding())

		local dispose4 = createEffect(function(scope)
			sum = sum + getB(scope)
		end)
		local dispose5 = createEffect(function(scope)
			sum = sum + getB(scope)
		end)
		local dispose6 = createEffect(function(scope)
			sum = sum + getB(scope)
		end)

		task.wait()
		expect(sum).toEqual(9 + getPadding())

		local dispose7 = createEffect(function(scope)
			sum = sum + getC(scope)
		end)
		local dispose8 = createEffect(function(scope)
			sum = sum + getC(scope)
		end)
		local dispose9 = createEffect(function(scope)
			sum = sum + getC(scope)
		end)

		task.wait()
		expect(sum).toEqual(18 + getPadding())

		setA(getB(false))
		task.wait()
		expect(sum).toEqual(24 + getPadding())

		setB(getC(false))
		task.wait()
		expect(sum).toEqual(33 + getPadding())

		setC(getA(false))
		task.wait()
		expect(sum).toEqual(39 + getPadding())

		dispose1()
		dispose2()
		dispose3()
		dispose4()
		dispose5()
		dispose6()
		dispose7()
		dispose8()
		dispose9()

		task.wait()
		expect(sum).toEqual(39 + getPadding())

		setA(1)
		setB(2)
		setC(3)

		task.wait()
		expect(sum).toEqual(39 + getPadding())
	end

	task.wait()
	expect(sum).toBe(39 * RUNS)
end)

it("should execute effects once per state update for complex graphs (with effect batching enabled)", function()
	local sum = 0

	local getA, setA = createSignal(1)
	local getB, setB = createSignal(2)
	local getC, setC = createSignal(3)

	local getD = createComputed(function(scope)
		return getA(scope) + getB(scope) + getC(scope)
	end)
	local getE = createComputed(function(scope)
		return getA(scope) + getB(scope) + getC(scope) + getD(scope)
	end)
	local getF = createComputed(function(scope)
		return getA(scope) + getB(scope) + getC(scope) + getD(scope) + getE(scope)
	end)
	local getG = createComputed(function(scope)
		return getE(scope) + getF(scope)
	end)
	local getH = createComputed(function(scope)
		return getF(scope) + getG(scope)
	end)
	local getI = createComputed(function(scope)
		return getF(scope) + getG(scope) + getA(scope)
	end)
	local getJ = createComputed(function(scope)
		return getF(scope) + getH(scope) + getD(scope) + getI(scope)
	end) -- J = 151

	for i = 1, RUNS do
		local function getPadding()
			return ((3 * 151) + (3 * 177)) * (i - 1)
		end

		local dispose1 = createEffect(function(scope)
			sum = sum + getJ(scope)
		end)
		local dispose2 = createEffect(function(scope)
			sum = sum + getJ(scope)
		end)
		local dispose3 = createEffect(function(scope)
			sum = sum + getJ(scope)
		end)

		task.wait()
		expect(sum).toEqual((3 * 151) + getPadding())

		batch(function()
			setA(getB(false)) -- J = 177
			setB(getC(false)) -- J = 202
			setC(getA(false)) -- J = 177
		end)

		task.wait()
		expect(sum).toEqual((3 * 151) + (3 * 177) + getPadding())

		dispose1()
		dispose2()
		dispose3()

		task.wait()
		expect(sum).toEqual((3 * 151) + (3 * 177) + getPadding())

		batch(function()
			setA(1)
			setB(2)
			setC(3)
		end)

		task.wait()
		expect(sum).toEqual((3 * 151) + (3 * 177) + getPadding())
	end

	task.wait()
	expect(sum).toEqual(((3 * 151) + (3 * 177)) * RUNS)
end)

it("should execute effects once per state update for complex graphs", function()
	local sum = 0

	local getA, setA = createSignal(1)
	local getB, setB = createSignal(2)
	local getC, setC = createSignal(3)

	local getD = createComputed(function(scope)
		return getA(scope) + getB(scope) + getC(scope)
	end)
	local getE = createComputed(function(scope)
		return getA(scope) + getB(scope) + getC(scope) + getD(scope)
	end)
	local getF = createComputed(function(scope)
		return getA(scope) + getB(scope) + getC(scope) + getD(scope) + getE(scope)
	end)
	local getG = createComputed(function(scope)
		return getE(scope) + getF(scope)
	end)
	local getH = createComputed(function(scope)
		return getF(scope) + getG(scope)
	end)
	local getI = createComputed(function(scope)
		return getF(scope) + getG(scope) + getA(scope)
	end)
	local getJ = createComputed(function(scope)
		return getF(scope) + getH(scope) + getD(scope) + getI(scope)
	end) -- J = 151

	for i = 1, RUNS do
		local function getPadding()
			return ((3 * 151) + (3 * 177) + (3 * 202) + (3 * 177)) * (i - 1)
		end

		local dispose1 = createEffect(function(scope)
			sum = sum + getJ(scope)
		end)
		local dispose2 = createEffect(function(scope)
			sum = sum + getJ(scope)
		end)
		local dispose3 = createEffect(function(scope)
			sum = sum + getJ(scope)
		end)

		task.wait()
		expect(sum).toEqual((3 * 151) + getPadding())

		setA(getB(false)) -- J = 177
		task.wait()
		expect(sum).toEqual((3 * 151) + (3 * 177) + getPadding())

		setB(getC(false)) -- J = 202
		task.wait()
		expect(sum).toEqual((3 * 151) + (3 * 177) + (3 * 202) + getPadding())

		setC(getA(false)) -- J = 177
		task.wait()
		expect(sum).toEqual((3 * 151) + (3 * 177) + (3 * 202) + (3 * 177) + getPadding())

		dispose1()
		dispose2()
		dispose3()

		task.wait()
		expect(sum).toEqual((3 * 151) + (3 * 177) + (3 * 202) + (3 * 177) + getPadding())

		setA(1)
		setB(2)
		setC(3)

		task.wait()
		expect(sum).toEqual((3 * 151) + (3 * 177) + (3 * 202) + (3 * 177) + getPadding())
	end

	task.wait()
	expect(sum).toEqual(((3 * 151) + (3 * 177) + (3 * 202) + (3 * 177)) * RUNS)
end)

-- From: https://fluff.blog/2024/07/14/glitches-in-dynamic-reactive-graphs.html
it("should correctly handle processing updates for dynamic graphs", function()
	local getHasSubGraph, setHasSubGraph = createSignal(true)

	local getOuter = createComputed(function(scope): Signals.getter<boolean>?
		if getHasSubGraph(scope) then
			return createComputed(function(innerScope)
				return getHasSubGraph(innerScope)
			end)
		else
			return nil
		end
	end)

	local outerIsNil = 0
	local innerIsTrue = 0
	local innerIsFalse = 0

	local dispose = createEffect(function(scope)
		local getInner = getOuter(scope)
		if getInner == nil then
			outerIsNil += 1
		else
			if getInner(false) then
				innerIsTrue += 1
			else
				innerIsFalse += 1
			end
		end
	end)

	task.wait()
	expect(outerIsNil).toEqual(0)
	expect(innerIsTrue).toEqual(1)
	expect(innerIsFalse).toEqual(0)

	setHasSubGraph(false)

	task.wait()
	expect(outerIsNil).toEqual(1)
	expect(innerIsTrue).toEqual(1)
	expect(innerIsFalse).toEqual(0)

	dispose()

	task.wait()
	expect(outerIsNil).toEqual(1)
	expect(innerIsTrue).toEqual(1)
	expect(innerIsFalse).toEqual(0)
end)

it("should support nested ephemeral signals", function()
	local getOuterEnabled, setOuterEnabled = createSignal(false)

	local getEnabled = createComputed(function(scope)
		local getInnerEnabled = createComputed(function(innerScope)
			return getOuterEnabled(innerScope)
		end)
		return getInnerEnabled(scope)
	end)

	expect(getEnabled(false)).toEqual(false)

	task.wait()
	task.wait()
	task.wait()

	setOuterEnabled(true)
	task.wait()

	expect(getEnabled(false)).toEqual(true)
end)

it("should not compute until first read", function()
	local getSignal, setSignal = createSignal(1)
	local computeCount = 0

	local getComputed = createComputed(function(scope)
		computeCount += 1
		return getSignal(scope) * 2
	end)

	expect(computeCount).toEqual(0) -- not computed until it is read

	setSignal(2)
	task.wait()
	expect(computeCount).toEqual(0) -- still not read

	local value = getComputed(false) -- now it computes as the getter reads it
	expect(computeCount).toEqual(1)
	expect(value).toEqual(4)
end)

it("should handle setting signals inside effects", function()
	local getSignalA, setSignalA = createSignal(0)
	local getSignalB, setSignalB = createSignal(0)
	local effectACount = 0
	local effectBCount = 0

	local dispose2 = createEffect(function(scope)
		getSignalB(scope)
		effectBCount += 1
	end)

	local dispose1 = createEffect(function(scope)
		local val = getSignalA(scope)
		effectACount += 1
		if val > 0 then
			setSignalB(val * 2)
		end
	end)

	task.wait()
	expect(effectACount).toEqual(1)
	expect(effectBCount).toEqual(1)

	setSignalA(5)
	task.wait()
	expect(getSignalB(false)).toEqual(10)
	expect(effectACount).toEqual(2)
	expect(effectBCount).toEqual(2)

	dispose1()
	dispose2()
end)

it("should support chained signal updates across effects", function()
	local getA, setA = createSignal(0)
	local getB, setB = createSignal(0)
	local getC, setC = createSignal(0)

	local dispose1 = createEffect(function(scope)
		local val = getA(scope)
		if val > 0 then
			setB(val + 1)
		end
	end)

	local dispose2 = createEffect(function(scope)
		local val = getB(scope)
		if val > 0 then
			setC(val + 1)
		end
	end)

	task.wait()
	expect(getA(false)).toEqual(0)
	expect(getB(false)).toEqual(0)
	expect(getC(false)).toEqual(0)

	setA(1)
	task.wait()
	expect(getA(false)).toEqual(1)
	expect(getB(false)).toEqual(2)
	expect(getC(false)).toEqual(3)

	dispose1()
	dispose2()
end)

it("should handle reading same signal multiple times in one scope", function()
	local getSignal, setSignal = createSignal(1)
	local count = 0

	local dispose = createEffect(function(scope)
		local _a = getSignal(scope)
		local _b = getSignal(scope)
		local _c = getSignal(scope)
		count += 1
	end)

	task.wait()
	expect(count).toEqual(1)

	setSignal(2)
	task.wait()
	expect(count).toEqual(2) -- should only run once, not three times

	dispose()
end)

it("should handle multiple effects depending on same computed", function()
	local get, set = createSignal(1)
	local getComputed = createComputed(function(scope)
		return get(scope) * 2
	end)

	local count1, count2, count3 = 0, 0, 0

	local dispose1 = createEffect(function(scope)
		getComputed(scope)
		count1 += 1
	end)

	local dispose2 = createEffect(function(scope)
		getComputed(scope)
		count2 += 1
	end)

	local dispose3 = createEffect(function(scope)
		getComputed(scope)
		count3 += 1
	end)

	task.wait()
	expect(count1).toEqual(1)
	expect(count2).toEqual(1)
	expect(count3).toEqual(1)

	set(5)
	task.wait()
	expect(count1).toEqual(2)
	expect(count2).toEqual(2)
	expect(count3).toEqual(2)

	dispose1()
	dispose2()
	dispose3()
end)

it("should lazily initialize signals with function values", function()
	local initCount = 0

	local get, set = createSignal(function()
		initCount += 1
		return 100
	end)

	expect(initCount).toEqual(0) -- not yet initialized

	local value = get(false)
	expect(value).toEqual(100)
	expect(initCount).toEqual(1) -- initialized

	get(false)
	expect(initCount).toEqual(1) -- should only init once

	set(200)
	expect(get(false)).toEqual(200)
	expect(initCount).toEqual(1)
end)

it("should handle table values with reference equality", function()
	local table1 = { value = 1 }
	local getSignal, setSignal = createSignal(table1)
	local count = 0

	local dispose = createEffect(function(scope)
		getSignal(scope)
		count += 1
	end)

	task.wait()
	expect(count).toEqual(1)

	setSignal(table1) -- same reference, should not trigger
	task.wait()
	expect(count).toEqual(1)

	local table2 = { value = 1 } -- different table reference, same contents, should trigger
	setSignal(table2)
	task.wait()
	expect(count).toEqual(2)

	setSignal(table2) -- same reference again, should not trigger
	task.wait()
	expect(count).toEqual(2)

	dispose()
end)

it("should recompute only when dependencies actually change", function()
	local getSignalA, setSignalA = createSignal(1)
	local getSignalB, setSignalB = createSignal(2)
	local computeCount = 0

	local getComputed = createComputed(function(scope)
		computeCount += 1
		return getSignalA(scope) + getSignalB(scope)
	end)

	expect(getComputed(false)).toEqual(3)
	expect(computeCount).toEqual(1)

	setSignalA(1) -- same value, should not recompute
	task.wait()
	expect(computeCount).toEqual(1)

	setSignalB(2) -- same value, should not recompute
	task.wait()
	expect(computeCount).toEqual(1)

	setSignalA(5) -- different value, should recompute
	expect(getComputed(false)).toEqual(7)
	expect(computeCount).toEqual(2)
end)

it("should handle reading signals after all effects are disposed", function()
	local getSignalA, setSignalA = createSignal(1)
	local getSignalB, setSignalB = createSignal(2)
	local count = 0

	local dispose1 = createEffect(function(scope)
		getSignalA(scope)
		count += 1
	end)

	local dispose2 = createEffect(function(scope)
		getSignalB(scope)
		count += 1
	end)

	task.wait()
	expect(count).toEqual(2)

	dispose1()
	dispose2()

	task.wait()
	expect(count).toEqual(2)

	-- signals should still be readable and settable
	expect(getSignalA(false)).toEqual(1)
	expect(getSignalB(false)).toEqual(2)

	setSignalA(10)
	setSignalB(20)

	expect(getSignalA(false)).toEqual(10)
	expect(getSignalB(false)).toEqual(20)

	task.wait()
	expect(count).toEqual(2) -- no phantom effects
end)

it("should handle nested batch calls", function()
	local getA, setA = createSignal(1)
	local getB, setB = createSignal(2)
	local getC, setC = createSignal(3)
	local count = 0

	local dispose = createEffect(function(scope)
		getA(scope)
		getB(scope)
		getC(scope)
		count += 1
	end)

	task.wait()
	expect(count).toEqual(1)

	batch(function()
		setA(10)
		batch(function()
			setB(20)
			setC(30)
		end)
	end)

	task.wait()
	expect(count).toEqual(2) -- should only run once for all updates
	expect(getA(false)).toEqual(10)
	expect(getB(false)).toEqual(20)
	expect(getC(false)).toEqual(30)

	dispose()
end)


local validationEnabled = _G.__SIGNALS_VALIDATION_ENABLED__ or _G.__DEV__

local function handleNoYieldValidation(co: thread, ok: boolean, ...)
	if not ok then
		local err = (...)
		if typeof(err) == "string" then
			error(debug.traceback(co, err), 2)
		else
			error(tostring(err), 2)
		end
	end

	if coroutine.status(co) ~= "dead" then
		error(debug.traceback(co, "Attempted to yield!"), 2)
	end

	return ...
end

local function callWithValidation<Args..., Rets...>(fn: (Args...) -> Rets..., ...: Args...): Rets...
	local co = coroutine.create(fn)
	return handleNoYieldValidation(co, coroutine.resume(co, ...))
end

local function callWithoutValidation<Args..., Rets...>(fn: (Args...) -> Rets..., ...: Args...): Rets...
	return fn(...)
end

return if validationEnabled then callWithValidation else callWithoutValidation :: never


local Signals = require(script.Signals)

export type getter<T> = Signals.getter<T>
export type setter<T> = Signals.setter<T>
export type update<T> = Signals.update<T>
export type equals<T> = Signals.equals<T>
export type dispose = Signals.dispose

export type scope = Signals.scope

return Signals


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Packages = ReplicatedStorage:FindFirstChild("Packages") or error("Packages folder not found")

local JestPackages = Packages:QueryDescendants("ModuleScript#Jest")
assert(#JestPackages > 0, "No Jest packages found")
local Jest = require(JestPackages[1])

local roots = Packages:QueryDescendants(`[Name="jest.config"]`)

assert(#roots > 0, "No test roots found")
print(`Running tests for {#roots} packages...`)

local status, result = Jest.runCLI(Packages, {
	ci = true,
	verbose = false,
}, roots):awaitStatus()

if status == "Rejected" then
	print(result)
	error("Tests failed")
elseif result.results.numFailedTestSuites > 0 or result.results.numFailedTests > 0 then
	error(`Tests failed: {result.results.numFailedTests} failed tests, {result.results.numFailedTestSuites} failed suites`)
end


-- this limits the total number of regex tests that will be parsed
-- from the pcre2 test file.
local MAX = 100000

local function parseTestFile(file)
	local testCaseList = {}

	local start = file:find("^/")

	local count = 0
	while start ~= nil do
		count = count + 1

		local ending = file:find("/", start + 1)
		local regexSource = file:sub(start + 1, ending - 1)
		local endOfRegexSourceLine = file:find("\n", ending + 1)
		local flags = file:sub(ending + 1, endOfRegexSourceLine - 1)

		local nextLineStart = endOfRegexSourceLine + 1
		local nextLineEnd = file:find("\n", endOfRegexSourceLine + 1)
		local line = file:sub(nextLineStart, nextLineEnd - 1)

		local regexTests = {}
		local currentTest = nil

		while line ~= "" do
			local matchIndex, matchValue = line:match("^([%d ]%d+): ?(.*)$")
			if line == "No match" then
				currentTest.matches = nil
			elseif matchIndex ~= nil then
				assert(
					currentTest.matches,
					"error parsing regex " .. tostring(count) .. " tests: '" .. regexSource .. "'"
				)
				table.insert(currentTest.matches, {
					index = tonumber(matchIndex),
					match = matchValue,
				})
			else
				if currentTest ~= nil then
					table.insert(regexTests, currentTest)
				end
				currentTest = {
					input = line:match("^ *(.+)$"),
					matches = {},
				}
			end
			nextLineStart = nextLineEnd + 1
			nextLineEnd = file:find("\n", nextLineStart)
			line = file:sub(nextLineStart, nextLineEnd - 1)
		end

		if currentTest ~= nil then
			table.insert(regexTests, currentTest)
		end

		local regexInfo = {
			source = regexSource,
			flags = flags,
		}

		table.insert(testCaseList, {
			source = regexSource,
			flags = flags,
			tests = regexTests,
		})

		if regexInfo.source == nil then
			error("\n\nerror at count = " .. count .. "\n\n")
		end

		start = file:find("\n/", nextLineEnd)
		if start then
			start = start + 1
		end

		if count >= MAX then
			break
		end
	end

	return testCaseList
end

return parseTestFile


local ProcessService = game:GetService("ProcessService")
local Root = script.Parent.LuauRegExpTestModel

local Packages = Root.Packages
local TestEZ = require(Root.Packages.Dev.TestEZ)

-- Run all tests, collect results, and report to stdout.
local result = TestEZ.TestBootstrap:run(
	{ Packages.RegExp },
	TestEZ.Reporters.TextReporterQuiet
)

if result.failureCount == 0 and #result.errors == 0 then
	ProcessService:ExitAsync(0)
else
	ProcessService:ExitAsync(1)
end


local ESCAPES = {
	["\n"] = "n",
	["\r"] = "r",
	["\t"] = "t",
	["\f"] = "f",
	["\a"] = "a",
	["\v"] = "v",
	["\\"] = "\\",
}
local ESCAPE_CHARS = {}
for char in pairs(ESCAPES) do
	table.insert(ESCAPE_CHARS, char)
end
local ESCAPE_CLASS = ("[%s%%c]"):format(table.concat(ESCAPE_CHARS, ""))

local function escapeString(str)
	return str:gsub(ESCAPE_CLASS, function(match)
		return "\\" .. (ESCAPES[match] or ("%03d"):format(match:byte()))
	end)
end

return escapeString


local RegEx = require(script.Parent.RegEx)
type Array<T> = { [number]: T }

type RegExpExecArray = Array<string> & { index: number?, input: string?, n: number }

export type RegExp = {
    exec: (self: RegExp, input: string) -> RegExpExecArray | nil,
    test: (self: RegExp, input: string) -> boolean,
}

local RegExp = {}
local RegExpMetatable = {
    __index = RegExp,
    __tostring = function(self)
        return tostring(self._innerRegEx)
    end,
}

function RegExp:exec(str: string): RegExpExecArray | nil
    local match = self._innerRegEx:match(str)
    if not match then
        return nil
    end

    local index = match:span()
    local groups = match:grouparr()

    local matches = { groups[0] }
    for i = 1, groups.n do
        matches[i + 1] = groups[i]
    end
    matches.n = groups.n + 1
    matches.index = index
    matches.input = str
    return matches
end

function RegExp:test(str: string): boolean
    return self:exec(str) ~= nil
end

local function new(_self, pattern: RegExp | string, flags: string?)
    flags = flags or ""
    local innerRegEx = RegEx.new(pattern, flags)
    local object = {
        source = pattern,
        ignoreCase = (flags :: string):find("i") ~= nil,
        global = (flags :: string):find("g") ~= nil,
        multiline = (flags :: string):find("m") ~= nil,
        _innerRegEx = innerRegEx,
    }

    return setmetatable(object, RegExpMetatable)
end

-- FIXME: Capture this as a local variable before returning, else a luau bug
-- prevents __call from being understood: https://jira.rbx.com/browse/CLI-40294
local interface = setmetatable(RegExp, {
    __call = new,
})

return interface


local RegExp = require(script["Regexp.global"])

export type RegExp = RegExp.RegExp

return RegExp


--!strict

--[[
	Written by @TenebrisNoctua.
	Handles logging messages to the output with different methods.
--]]

--// Variables

local MessageConstructor = require('./MessageConstructor')
local Logger = {}

--/// Module Functions

--[[
	Errors and halts execution of the current thread.
]]
function Logger.logError(messageId: string, includeTraceback: boolean, formatTable: {any}?, ...): never
	error(MessageConstructor(messageId, includeTraceback, formatTable, ...), 0)
end

--[[
	Errors without halting the current thread.
]]
function Logger.logErrorNonFatal(messageId: string, includeTraceback: boolean, formatTable: {any}?, ...): ()
	task.spawn(error, MessageConstructor(messageId, includeTraceback, formatTable, ...), 0)
end

--[[
	Displays a warning message to the output.
]]
function Logger.logWarn(messageId: string, includeTraceback: boolean, formatTable: {any}?, ...)
	warn(MessageConstructor(messageId, includeTraceback, formatTable, ...))
end

return Logger


--!strict

--[[
	Written by @TenebrisNoctua.
	Stores templates for logging messages.
--]]

return {
	attemptToModifyReadOnlyTable = "Cannot modify a read only table.",
	attemptToCreateObjectFromAbstractClass = "Cannot create an object from an abstracted class.",
	attemptToExtendAFinalClass = "Cannot create an inherited class from a final class.",
	cannotCallFunctionError = "Cannot call function \"%s\" directly.",
	cannotCallFunctionFrom = "Cannot call function \"%s\" from \"%s\".",
	cannotFindBaseClassMethod = "Property \"%s\" cannot be found or is not a function in the base class \"%s\".",
	classAlreadyExists = "Class \"%s\" already exists.",
	classDataNotTable = "Given classData value for the class \"%s\" is not a table.",
	classDataContainsMetatable = "Given classData value for the class \"%s\" contains a metatable.",
	classMemberCannotSetToNil = "Class members cannot be set to nil.",
	classMemberNotFound = "This class has no member named \"%s\".",
	classNameNotString = "Class name is not a string or is nil.",
	classNotFound = "A class with the given name cannot be found.",
	classNoMatchingFunctionError = "No match for %s function in this class.",
	classObjectLocked = "This class object has been locked.",
	classPropertyIsPrivate = "Property \"%s\" is private in this class.",
	classPropertyIsProtected = "Property \"%s\" is protected in this class.",
	classPropertyIsInternal = "Property \"%s\" is internal in this class. You cannot access it.",
	classPropertyRedeclaration = "Cannot redeclare property \"%s\".",
	classTypeMismatch = "The class type \"%s\" does not match with the intended class type \"%s\".",
	expectedFunctionError = "Expected function, got: %s.",
	extendsDeprecated = "Extends is now deprecated. Use the \"class\" function instead.",
	invalidAccessSpecifierError = "Given value is not a valid access specifier.",
	invalidAccessSpecifierInClassData = "Given classData value for the class \"%s\" contains invalid access specifier(s) or function(s).",
	invalidClassNewIndex = "Cannot set a new property for the class \"%s\" that is an invalid access specifier or a function.",
	invalidConstructor = "The current function does not match with a valid constructor function in the class \"%s\", or within its inherited classes.",
	invalidDestructorCall = "Cannot call Destroy without an object argument. Try calling the function with the ':' operator.",
	invalidInheritation = "Cannot call super in a multi-inherited class, or in a non-inherited class.",
	invalidModifierArgument = "Given argument is not a class table.",
	invalidPropertyDecleration = "Cannot create reserved property \"%s\".",
	invalidSuperCall = "Cannot call super without an object argument. Try calling the function with the ':' operator.",
	illegalModifierCombination = "Cannot create an illegal combination of modifiers: %s",
	nonNativeOperatorCall = "Operator function %s cannot be called without its operator.",
	overloadfunctionArgumentMismatch = "A function to handle the given number of arguments (%d) have not been provided for the overloaded function: \"%s\"",
	overloadfunctionNameNotSet = "A function name has not been set for the overloaded function.",
	overloadfunctionTableNotGiven = "A function table has not been given for the overloaded function.",
	staticMemberNameNotSet = "A name has not been set for the static member.",
	typeofObjectNotFound = "The given object's type cannot be found.",
	unhandledError = "An unhandled error occured in \"%s\": %s",
	unknownError = "An unknown error has occured."
}


--!native
--!strict

--[[
	Written by @TenebrisNoctua.
	Utilities for Class++.
--]]

--// Variables

local Type = require('./Type')

local type = Type.type
local typeof = Type.typeof

local Util = {}

--// Types

type internalClass = Type.internalClass

--// Module Functions

function Util.getMethodContext(level: number): ((any) -> (any), string)
	local f: (any) -> (any)
	local n: string

	pcall(function()
		f = debug.info(level, 'f')
		n = debug.info(level, 'n')
	end)

	return f, n
end

function Util.checkInheritance(class: internalClass, classOrMethod: any | () -> ()): boolean
	for _, inherited in class.Inherits do
		if inherited == classOrMethod or (inherited :: any)[classOrMethod] then
			return true
		end
	end

	return false
end

function Util.checkFriendship(class: internalClass, methodName: string, method: (any) -> (any), classes: {[string]: internalClass}): boolean
	for _, friend in class.Friends do
		if type(friend) == "Class" and friend[methodName] then
			return true
		elseif typeof(friend) == "string" then
			if classes[friend] and classes[friend][methodName] then
				return true
			else
				continue
			end
		elseif typeof(friend) == "function" and friend == method then
			return true
		end
	end

	return false
end

function Util.inClassScope(class: internalClass, includeInherited: boolean, includeFriend: boolean, classes: {[string]: internalClass}?, defaultLevel: number?): boolean
	local level: number = defaultLevel or 6

	while true do
		local method, methodName = Util.getMethodContext(level)
		if not method then return false end

		if class[methodName] == method then return true end
		if includeInherited and Util.checkInheritance(class, method) or includeFriend and classes and Util.checkFriendship(class, methodName, method, classes) then return true end
		if method == typeof or methodName == "overloadHandler" and table.find(class.overloadlist, method) then return true end

		level += 1
	end

	return false
end

function Util.binarySearch(array: {any}, value: any): any
	local Low: number = 1
	local High: number = #array

	while Low <= High do
		local Middle: number = Low + math.floor((High - Low) / 2)
		local MiddleValue: number = array[Middle]

		if value < MiddleValue then
			High = Middle - 1
		elseif MiddleValue < value then
			Low = Middle + 1
		else
			while Middle >= 1 and not (array[Middle] < value or value < array[Middle]) do
				Middle -= 1
			end
			return Middle + 1
		end
	end

	return
end

return Util


--!native
--!strict

--[[
	Written by @TenebrisNoctua.
	The main module of Class++.
--]]

--[[
	WARNING: Unless you know exactly what you're doing, it is not recommended to modify the source code below.
	If you ever encounter a bug due to a modification that you have made (which is very likely to happen), DO NOT CREATE A BUG REPORT.
]]

--// Variables

local Util = require("@self/Utility")
local Logger = require("@self/Log/Logger")
local Type = require("@self/Type")

local LogError = Logger.logError
local LogErrorNonFatal = Logger.logErrorNonFatal
local LogWarn = Logger.logWarn

local classes = {} :: {[string]: internalClass}
local classpp = {version = {major = 2, minor = 1, patch = 0, beta = false}, autoObjectDestruction = false} :: classPP

local accessSpecifiers: accessSpecifiers = {"Friend", "Internal", "Private", "Protected", "Public"}
local overloadableOperators: overloadableOperators = {"operator_add", "operator_div", "operator_eq", "operator_idiv", "operator_mod", "operator_mul", "operator_pow", "operator_sub", "operator_unm"}

--// Types

type classPP = Type.classPP<typeof(Util), typeof(Type)>
type accessSpecifiers = Type.accessSpecifiers
type overloadableOperators = Type.overloadableOperators

export type classData<T> = Type.classData<T>
export type class<T> = Type.class<T, T>
export type object<T> = Type.object<T>

type defaultClassData = Type.defaultClassData
type objectData = Type.objectData
type internalClass = Type.internalClass

type anyTable = Type.anyTable
type deepAnyTable = Type.deepAnyTable
type metatable<T, V = any?> = setmetatable<{T}, {V}>

--// Local Functions

-- A deep copy function for tables, with support for metatables.
local function deepCopy<T>(original: T, copyMetatable: boolean?): T
	local original: anyTable = original
	local copy = table.clone(original)

	for key, value in original do
		if type(value) ~= "table" then continue end
		local metatable = getmetatable(value :: anyTable)

		if not metatable then
			copy[key] = deepCopy(value, true)
		elseif not copyMetatable then
			local success, result = pcall(setmetatable, value, {})
			local copyTable

			if success then
				copyTable = result
			else
				copyTable = {}
				for key, val in value do
					copyTable[key] = val
				end
			end

			copy[key] = deepCopy(copyTable, true)
		else
			copy[key] = value
		end
	end

	return copy
end

-- Recursively cleans the given table.
local function recursiveClean(tbl: anyTable)
	for key, value in tbl do
		local keyType = typeof(key)
		local valueType = typeof(value)

		if keyType == "Instance" then key:Destroy() end

		if valueType == "table" then
			for _, v in tbl do
				if v == value then return end
			end
			recursiveClean(value)
		elseif valueType == "Instance" then
			value:Destroy()
		elseif valueType == "RBXScriptConnection" then
			value:Disconnect()
		elseif valueType == "thread" then
			pcall(task.cancel, value)
		end

		tbl[key] = nil
	end
end

-- Detects when an object gets garbage collected.
local function runOnGarbageCollection(object: unknown, callback: () -> ())
	local alive_test = setmetatable({object}, {__mode = "v"})
	coroutine.wrap(function() -- Unlike task.spawn, this method is faster at creating a new thread, though a bit more unreliable.
		repeat task.wait(1) until alive_test[1] == nil
		callback()
	end)()
end

-- Cleans the given object's objectData table.
local function objectCleaner(objectData: objectData): objectData
	for accessSpecifier, attributesData in objectData do
		if accessSpecifier == "Internal" then continue end
		recursiveClean(attributesData)
		objectData[accessSpecifier] = nil
	end

	objectData.Internal.__locked = true
	return objectData
end

-- Runs on garbage collection.
local function garbageCollectionCallback(objectData: objectData)
	if objectData.Internal.__locked then
		table.clear(objectData)
	else
		if objectData.Internal.destructor then
			-- We create a copy of the object here, because giving a direct reference to the object will cause this function to never be run.
			-- This copy will immediately be deleted after destructor runs.
			local objectCopy = setmetatable({}, {__metatable = "This table is locked."}) :: metatable<{[string]: any}, {__metatable: string}>

			for k, v in objectData.Protected do
				objectCopy[k] = v
			end
			for k, v in objectData.Public do
				if k == "Destroy" or k == "super" then continue end
				objectCopy[k] = v
			end
			for k, v in objectData.Private do
				objectCopy[k] = v
			end

			local success, err = pcall(objectData.Internal.destructor, objectCopy)
			if not success then LogErrorNonFatal("unhandledError", true, {"destructor", err}) end

			objectCleaner(objectData)
			table.clear(objectCopy)

			objectCopy = nil
		end

		table.clear(objectData)
	end
end

-- Recursively finds the parent function, and calls it.
local function recursiveFindParentFunction(obj: objectData, method: (...any) -> (any), class: internalClass, findType: "constructor" | "function", methodName: string?, ...)
	local inheritedClass: internalClass? = class.Inherits[1]
	if not class.classData then return end

	if findType == "constructor" then
		if inheritedClass and class.classData.constructor == method and inheritedClass.classData then
			return inheritedClass.classData.constructor(obj, ...)
		elseif inheritedClass then
			return recursiveFindParentFunction(obj, method, inheritedClass, "constructor", nil, ...)
		else
			LogError("invalidConstructor", true, {Type.typeof(obj)})
		end
	else
		if inheritedClass and inheritedClass.classData then
			local inheritedClassData = inheritedClass.classData
			local foundPublic: (...any) -> (any?) = inheritedClassData.Public[methodName]
			local foundProtected: (...any) -> (any?) = inheritedClassData.Protected[methodName]

			if foundPublic and typeof(foundPublic) == "function" and class.classData.Public[methodName] == method then
				return foundPublic(obj, ...)
			elseif foundProtected and typeof(foundProtected) == "function" and (class.classData.Protected[methodName] == method or class.classData.Public[methodName] == method) and method ~= foundProtected then
				return foundProtected(obj, ...)
			else
				return recursiveFindParentFunction(obj, method, inheritedClass, "function", methodName, ...)
			end
		else
			LogError("cannotFindBaseClassMethod", true, {methodName, class.Name})
		end
	end
end

-- Sorts the classData into an objectData that can be used inside objects.
local function sortAttributes(classData: defaultClassData, className: string, class: internalClass): objectData
	local objectTbl: objectData = {
		Public = deepCopy(classData.Public),
		Private = deepCopy(classData.Private),
		Protected = deepCopy(classData.Protected),
		Internal = {
			__type = "Object",
			__objtype = className,
			__locked = false,
			constructor = classData.constructor,
			destructor = classData.destructor
		}
	}

	local inheritedClassNum = #class.Inherits
	local inheritedClass: internalClass = class.Inherits[1]
	local inheritedClassData = if inheritedClass then inheritedClass.classData else nil
	local classType = Type.typeof(class)
	local destructor = objectTbl.Internal.destructor

	function objectTbl.Public.Destroy(obj: objectData)
		local objectType = Type.type(obj)
		local objectClassType = Type.typeof(obj)

		if objectType ~= "Object" then LogError("invalidDestructorCall", true) end
		if objectClassType ~= classType then LogError("classTypeMismatch", true, {objectType, classType}) end
		if Util.getMethodContext(5) == objectTbl.Internal.constructor then LogError("cannotCallFunctionFrom", true, {"Destroy", "constructor"}) end

		if destructor then
			local success, err = pcall(destructor, obj)
			if not success then LogErrorNonFatal("unhandledError", true, {"destructor", err}) end
		end

		objectCleaner(objectTbl)
	end

	function objectTbl.Public.super(obj: objectData, ...)
		local objectType = Type.type(obj)
		local objectClassType = Type.typeof(obj)

		if objectType ~= "Object" then LogError("invalidSuperCall", true) end
		if objectClassType ~= classType then LogError("classTypeMismatch", true, {objectType, classType}) end
		if inheritedClassNum > 1 or not inheritedClass then LogError("invalidInheritation", true) end
		if not inheritedClassData then return end

		local method, currentMethod = Util.getMethodContext(5)
		if method == destructor then LogError("cannotCallFunctionFrom", true, {"super", currentMethod}) end

		if currentMethod == "constructor" then
			return recursiveFindParentFunction(obj, method, class, "constructor", nil, ...)
		else
			return recursiveFindParentFunction(obj, method, class, "function", currentMethod, ...)
		end
	end

	return objectTbl
end

-- Merge the classDatas in the given classDataTable with the childClassData.
local function mergeClassDatas(classDataTable: {defaultClassData}, childClassData: defaultClassData): defaultClassData
	local newClassData = {Public = {}, Private = {}, Protected = {}}

	local function merge(classData: defaultClassData)
		if classData.Public then
			for attributeName, attributeData in classData.Public do
				newClassData.Public[attributeName] = attributeData
			end
		end
		if classData.Protected then
			for attributeName, attributeData in classData.Protected do
				newClassData.Protected[attributeName] = attributeData
			end
		end
	end

	local function finalize(classData: defaultClassData)
		for name, _ in newClassData.Protected do
			if newClassData.Public[name] or newClassData.Private[name] then
				newClassData.Protected[name] = nil
			end
		end

		if classData.constructor then newClassData.constructor = classData.constructor end
		if classData.destructor then newClassData.destructor = classData.destructor end

		newClassData.Private = classData.Private
		newClassData.Friend = classData.Friend
	end

	-- Merge all classData tables.
	for _, classData in ipairs(classDataTable) do
		merge(classData)
	end

	-- Merge the childClassData.
	merge(childClassData)

	-- Finalize the merge.
	finalize(childClassData)

	return newClassData
end

-- Returns the access specifier of an index.
local function returnAccessSpecifier(index: string, objectData: defaultClassData | objectData): string?
	if objectData.Public and objectData.Public[index] ~= nil then return "Public" end
	if objectData.Private and objectData.Private[index] ~= nil then return "Private" end
	if objectData.Protected and objectData.Protected[index] ~= nil then return "Protected" end
	if objectData.Internal and (objectData.Internal :: any)[index] ~= nil then return "Internal" end

	return nil
end

-- Finds the property that's been set to the given index. If given an access specifier string, it will try to find the property inside that access specifier.
local function findPropertyFromIndex(classData: defaultClassData | objectData, index: string, accessSpecifier: string?): any?
	local fAccessSpecifier = accessSpecifier or returnAccessSpecifier(index, classData)
	if not fAccessSpecifier then return end

	local attributesData = classData[fAccessSpecifier]
	return attributesData[index]
end

-- Checks if the given string is a valid access specifier.
local function checkValidAccessSpecifier(specifierString: string): (boolean, string?)
	local isFound = Util.binarySearch(accessSpecifiers, specifierString)
	return isFound ~= nil, accessSpecifiers[isFound]
end

-- Checks if the given index has already been set inside an access specifier.
local function checkRedeclaration(classData: defaultClassData, index: string?): (boolean, any?)
	if not classData then return false end

	if index then
		local fnumber = 0

		if classData.Public and classData.Public[index] then fnumber += 1 end
		if classData.Private and classData.Private[index] then fnumber += 1 end
		if classData.Protected and classData.Protected[index] then fnumber += 1 end
		if fnumber >= 2 then return true, index end
	else
		for _, attributesData in classData do
			if typeof(attributesData) ~= "table" then continue end
			for attributeKey, _ in attributesData do
				if checkRedeclaration(classData, attributeKey) then return true, attributeKey end
			end
		end
	end

	return false
end

-- Checks if there are any indexes inside the classData that aren't valid access specifiers or functions.
local function checkOutsideAccessSpecifierDecleration(classData: defaultClassData): boolean
	for index, _ in classData do
		if typeof(index) ~= "string" then return true end
		if not checkValidAccessSpecifier(index) and index ~= "constructor" and index ~= "destructor" then return true end
	end

	return false
end

-- Checks if the given classData is valid and can be used.
local function checkValidClassData(classData: defaultClassData, className: string)
	if typeof(classData) ~= "table" then LogError("classDataNotTable", false, {className}) end
	if getmetatable(classData) or not select(1, pcall(setmetatable, classData, {})) then LogError("classDataContainsMetatable", false, {className}) end

	local isAPropertyRedeclared: boolean, redeclaredIndex: any? = checkRedeclaration(classData)
	if isAPropertyRedeclared then LogError("classPropertyRedeclaration", false, redeclaredIndex) end

	if findPropertyFromIndex(classData, "Destroy") then LogError("invalidPropertyDecleration", false, {"Destroy"}) end
	if findPropertyFromIndex(classData, "super") then LogError("invalidPropertyDecleration", false, {"super"}) end
	if checkOutsideAccessSpecifierDecleration(classData) then LogError("invalidAccessSpecifierInClassData", false, {className}) end
end

-- Calls an operator function from the given name.
local function callOperator(functionName: string, obj: anyTable, obj2: anyTable?, silenceError: boolean?): boolean | any
	if typeof(obj) == "userdata" then
		local objFunction = obj[functionName] :: (...any) -> (any)
		if objFunction then
			return objFunction(obj, obj2)
		elseif not silenceError then
			LogError("classNoMatchingFunctionError", true, {functionName})
		end
	elseif typeof(obj2) == "userdata" then
		local objFunction = obj2[functionName] :: (...any) -> (any)
		if objFunction then
			return objFunction(obj2, obj)
		elseif not silenceError then
			LogError("classNoMatchingFunctionError", true, {functionName})
		end
	end

	return false
end

-- Sets the metamethods of an object.
local function setMetamethods(class: internalClass, objectData: objectData, methods: anyTable)
	local Internal = objectData.Internal
	methods.__metatable = 'The metatable is locked.'

	function methods:__index(index: string)
		if Internal.__locked == true then LogError("classObjectLocked", true) end

		local accessSpecifier = returnAccessSpecifier(index, objectData)
		local rprop = findPropertyFromIndex(objectData, index, accessSpecifier)
		local isOperatorCall = string.find(index, "operator")

		if accessSpecifier ~= "Public" then
			if accessSpecifier == "Private" and not Util.inClassScope(class, false, true, classes) then
				LogError("classPropertyIsPrivate", true, {index})
			elseif accessSpecifier == "Protected" and not Util.inClassScope(class, true, false) then
				LogError("classPropertyIsProtected", true, {index})
			elseif accessSpecifier == "Internal" and Util.getMethodContext(7) ~= Type.type and Util.getMethodContext(7) ~= Type.typeof then
				LogError("classPropertyIsInternal", true, {index})
			end
		end

		if isOperatorCall then
			local isFound = Util.binarySearch(overloadableOperators, index)
			if isFound and Util.getMethodContext(5) ~= callOperator then
				LogError("nonNativeOperatorCall", true, {index})
			end
		end

		if rprop ~= nil then return rprop elseif index ~= "constructor" and index ~= "destructor" and index ~= "__tostring" and not isOperatorCall then LogError("classMemberNotFound", true, {index}) end
		return
	end

	function methods:__newindex(index: string, value: any)
		if Internal.__locked == true then LogError("classObjectLocked", true) end
		if value == nil then LogError("classMemberCannotSetToNil", true) end

		local accessSpecifier = returnAccessSpecifier(index, objectData)
		if accessSpecifier ~= "Public" then
			if accessSpecifier == "Private" and not Util.inClassScope(class, false, true, classes) then
				LogError("classPropertyIsPrivate", true, {index})
			elseif accessSpecifier == "Internal" then
				LogError("classPropertyIsInternal", true, {index})
			elseif accessSpecifier == "Protected" and not Util.inClassScope(class, false, false) then
				LogError("classPropertyIsProtected", true, {index})
			end
		end

		local attributesData = objectData[accessSpecifier]
		local foundAttribute = attributesData[index]
		if foundAttribute == nil then LogError("classMemberNotFound", true, {index}) elseif foundAttribute == value then return end

		attributesData[index] = value
	end

	function methods:__tostring()
		if Internal.__locked == true then LogError("classObjectLocked", true) end

		local result = callOperator("__tostring", self, nil, true)
		if result then
			return result
		else
			return class.Name
		end
	end

	for _, operatorFuncName in overloadableOperators do
		if not objectData.Public[operatorFuncName] then continue end

		methods[string.gsub(operatorFuncName, "operator", "_")] = function(obj, obj2)
			if Internal.__locked == true then LogError("classObjectLocked", true) end
			return callOperator(operatorFuncName, obj, obj2)
		end
	end
end

local returnClassFunction: (className: string, ...any) -> any

local function createClassObject(newClassData: defaultClassData, className: string, inheritedClasses: {internalClass}?)
	local staticFunction = nil
	local overloadedFunctions: {(...any) -> (...any?)} = {}
	local class: internalClass = {
		Inherits = setmetatable(inheritedClasses or {}, { __tostring = function() return '{}' end, __newindex = function(t, i, v) local f = Util.getMethodContext(5); if f ~= createClassObject then LogError("attemptToModifyReadOnlyTable", false) end; rawset(t, i, v); return end, __metatable = {} }),
		Friends = setmetatable(newClassData.Friend or {}, { __tostring = function() return '{}' end, __newindex = function(t, i, v) local f = Util.getMethodContext(5); if f ~= createClassObject then LogError("attemptToModifyReadOnlyTable", false) end; rawset(t, i, v); return end, __metatable = {} }),
		Statics = setmetatable({}, {__tostring = function() return '{}' end, __newindex = function(t, i, v) local f = Util.getMethodContext(5); if f ~= staticFunction then LogError("attemptToModifyReadOnlyTable", false) end; rawset(t, i, v); return end, __metatable = {} }),
		Name = className,
		new = function() end,
		overload = function() end,
		static = function() end
	}

	newClassData.Public = setmetatable(newClassData.Public or {}, {__newindex = function(t, i, v)
		local previousAccessSpecifier = returnAccessSpecifier(i, newClassData)
		if previousAccessSpecifier then newClassData[previousAccessSpecifier][i] = nil end
		rawset(t, i, v)
	end}) :: any

	newClassData.Private = setmetatable(newClassData.Private or {}, {__newindex = function(t, i, v)
		local previousAccessSpecifier = returnAccessSpecifier(i, newClassData)
		if previousAccessSpecifier then newClassData[previousAccessSpecifier][i] = nil end
		rawset(t, i, v)
	end}) :: any

	newClassData.Protected = setmetatable(newClassData.Protected or {}, {__newindex = function(t, i, v)
		local previousAccessSpecifier = returnAccessSpecifier(i, newClassData)
		if previousAccessSpecifier then newClassData[previousAccessSpecifier][i] = nil end
		rawset(t, i, v)
	end}) :: any

	--// Class Metatable

	local methods = {
		__type = "Class",
		__classtype = className,
		__abstract = false,
		__final = false
	}

	function methods.__index(t: internalClass, i: string): any
		local methodContext = Util.getMethodContext(5)
		local isNotInClassScope = methodContext ~= Util.inClassScope
		local isNotCheckFriendShip = methodContext ~= Util.checkFriendship

		if isNotInClassScope then
			if i == "__type" then return end
			if i == "__abstract" then
				if classpp.abstract == methodContext or classpp.final == methodContext then
					return methods.__abstract
				end
			end
			if i == "__final" then
				if classpp.final == methodContext or classpp.abstract == methodContext or returnClassFunction == methodContext then
					return methods.__final
				end
			end
			if i == "classData" then
				if returnClassFunction == methodContext or createClassObject == methodContext or sortAttributes == methodContext or recursiveFindParentFunction == methodContext then
					return newClassData
				else
					LogError("classPropertyIsInternal", true, {"classData"})
				end
			end
		else
			if i == "overloadlist" then
				return overloadedFunctions
			end
		end

		local result = newClassData[i] or findPropertyFromIndex(newClassData, i)
		if not result then return class.Statics[i] end

		local resultType = typeof(result)
		if resultType == "function" and isNotInClassScope and isNotCheckFriendShip then
			LogError("cannotCallFunctionError", true, {i})
		elseif resultType == "table" then
			local metatable = getmetatable(result :: any)
			if metatable["__call"] and isNotInClassScope then
				LogError("cannotCallFunctionError", true, {i})
			end
		end

		return result
	end

	function methods.__newindex(t: internalClass, i: string, v: any)
		local methodContext = Util.getMethodContext(5)

		if i == "__type" then return end
		if i == "__abstract" then
			if classpp.abstract == methodContext and v == true then
				methods.__abstract = true
				class.new = function()
					LogError("attemptToCreateObjectFromAbstractClass", true)
				end
				return
			end
		end
		if i == "__final" then
			if classpp.final == methodContext and v == true then
				methods.__final = true
				return
			end
		end

		if not checkValidAccessSpecifier(i) and i ~= "constructor" and i ~= "destructor" then
			LogError("invalidClassNewIndex", true, {className})
		end

		newClassData[i] = v

		return v
	end

	--// Class Methods

	function class.new(...)
		local newObjData: objectData = sortAttributes(newClassData, className, class)
		local newObject = newproxy(true)
		local proxyMethods = getmetatable(newObject)

		if classpp.autoObjectDestruction then
			runOnGarbageCollection(newObject, function()
				garbageCollectionCallback(newObjData)
				newObjData = nil :: any
			end)
		end

		setMetamethods(class, newObjData, proxyMethods)

		if newObjData.Internal.constructor then newObjData.Internal.constructor(newObject, ...) end

		return newObject
	end

	function class.extends()
		LogError("extendsDeprecated", true)
	end

	function class.overload(accessSpecifier: string, name: string, functionTable: {(...any) -> (any)})
		local accessSpecifierCheck, specifierName = checkValidAccessSpecifier(accessSpecifier)
		if not accessSpecifierCheck or specifierName and specifierName == "Internal" then LogError("invalidAccessSpecifierError", true) end
		if not name then LogError("overloadfunctionNameNotSet", true) end
		if typeof(functionTable) ~= "table" or #functionTable == 0 then LogError("overloadfunctionTableNotGiven", true) end
		if not specifierName then return end

		for _, func in functionTable do
			if typeof(func) ~= "function" then LogError("expectedFunctionError", true, {typeof(func)}) end
		end

		local function overloadHandler(...)
			if Util.getMethodContext(4) == deepCopy then return end

			local args = {...}
			local f = false

			for _, func in functionTable do
				local argCount, isVariadic = debug.info(func, 'a')
				if argCount == #args or isVariadic then
					f = true
					return func(...)
				end
			end

			if not f and #args > 0 then LogErrorNonFatal("overloadfunctionArgumentMismatch", false, {#args, name}) end
		end

		newClassData[specifierName][name] = overloadHandler
		table.insert(overloadedFunctions, overloadHandler)
	end

	function class.static(accessSpecifier: string, name: string, property: any)
		local accessSpecifierCheck, specifierName = checkValidAccessSpecifier(accessSpecifier)
		if not accessSpecifierCheck or specifierName and specifierName == "Internal" or specifierName and specifierName == "Protected" then LogError("invalidAccessSpecifierError", true) end
		if not name then LogError("staticMemberNameNotSet", true) end
		if not class.Statics then return end

		local newObj = newproxy(true)
		local methods = getmetatable(newObj)

		function methods:__index(): any
			if accessSpecifier == "Private" and not Util.inClassScope(class, false, true, classes) then
				LogError("classPropertyIsPrivate", true, {name})
			end

			return property
		end

		function methods:__newindex(index: any, value: any)
			if accessSpecifier == "Private" and not Util.inClassScope(class, false, true, classes) then
				LogError("classPropertyIsPrivate", true, {name})
			end

			property = value
			return property
		end

		function methods:__call(...)
			if accessSpecifier == "Private" and not Util.inClassScope(class, false, true, classes) then
				LogError("classPropertyIsPrivate", true, {name})
			end

			if typeof(property) == "function" then
				return property(...)
			else
				return property
			end
		end

		function methods:__tostring()
			if accessSpecifier == "Private" and not Util.inClassScope(class, false, true, classes) then
				LogError("classPropertyIsPrivate", true, {name})
			end

			return tostring(property)
		end

		class.Statics[name] = newObj

		return newObj
	end

	staticFunction = class.static
	return setmetatable(class, methods)
end

local function constructClass(classDataArg: defaultClassData, className: string, otherClassDatas: {defaultClassData}?, otherClasses: {internalClass}?)
	checkValidClassData(classDataArg, className)

	local newClassData: defaultClassData = if not otherClassDatas then classDataArg else mergeClassDatas(otherClassDatas, classDataArg)
	local nCObj = createClassObject(newClassData, className, otherClasses)

	classes[className] = nCObj

	return nCObj
end

returnClassFunction = function(className: string, ...): any
	local argTable = {...}
	local count = #argTable
	local firstArg = argTable[1]

	if count >= 2 then
		local classDataTable: {defaultClassData} = {}
		local classTable: {internalClass} = {}

		for _, class: internalClass in argTable do
			if Type.type(class) ~= "Class" then continue end
			if class["__final"] then LogError("attemptToExtendAFinalClass", true) end
			if not class.classData then continue end

			table.insert(classDataTable, class.classData)
			table.insert(classTable, class)
		end

		return function(classDataArg: defaultClassData)
			return constructClass(classDataArg, className, classDataTable, classTable)
		end
	elseif count == 1 and Type.type(firstArg) == "Class" then
		local class: internalClass = firstArg
		if class["__final"] then LogError("attemptToExtendAFinalClass", true) end
		if not class.classData then return end

		local classDataTable: {defaultClassData} = {class.classData}
		local classTable: {internalClass} = {class}

		return function(classDataArg: defaultClassData)
			return constructClass(classDataArg, className, classDataTable, classTable)
		end
	else
		return constructClass(argTable[1], className)
	end
end

--// Module Functions

function classpp.class(className: string): any
	return function(...): any
		return returnClassFunction(className, ...)
	end
end

function classpp.abstract(classTable: {internalClass}): any
	if typeof(classTable) ~= "table" then LogError("invalidModifierArgument", true) end

	for _, class in classTable do
		if Type.type(class) ~= "Class" then LogError("typeofObjectIsNotClass", false) end
		if class["__final"] then LogError("illegalModifierCombination", true, {`abstract and final`}) end

		class["__abstract"] = true
	end

	return classTable[1]
end

function classpp.final(classTable: {internalClass}): any
	if typeof(classTable) ~= "table" then LogError("invalidModifierArgument", true) end

	for _, class in classTable do
		if Type.type(class) ~= "Class" then LogError("typeofObjectIsNotClass", false) end
		if class["__abstract"] then LogError("illegalModifierCombination", true, {`final and abstract`}) end

		class["__final"] = true
	end

	return classTable[1]
end

function classpp.getClass(className: string): any
	if typeof(className) ~= "string" then LogError("classNameNotString", false) end
	if not classes[className] then LogError("classNotFound", false) end
	return classes[className]
end

classpp.Type = Type
classpp.Util = Util

return classpp


--!strict
--!native

--[[
	Penumbra: Init / Settings / Launch Parameters

	This module handles inputs such as launch parameters provided via the Pajamas runtime,
	a configurator for the Penumbra project, and provides settings & launch parameters to
	every module calling afterwards.

		How to launch:
	local pajamas = require(`./path/to/pajamas`)
	pajamas("penumbra", "main", "640", "360", "8")

	The above example calls into pajamas via a server or client script,
	which then passes the launch parameters for penumbra.
	"main" is the module name intended to run on project startup.
	"640" is the X resolution I specified for my video output.
	"360" is the Y resolution I specified for my video output.
	"8" is the number of render threads I wanted to run penumbra with.

	Note that all inputs are string based and string representations of
	numbers and booleans will be type coerced, just as if they were
	being launched from the terminal.

	Ensure that the .pajamasrc runtime table is correctly configured for
	the context you wish to run this in.
]]

export type self = {
	read Display_Res:		vector,
	read Render_Threads:	number,
	read Display_Resample:	vector,
	read Actor_Res:			vector,
	read Actor_RenderNum:	number
}

export type params = {
	[string]: 		any,
	i:				{[number]: any},
	read Path: 		string,
	read X: 		number,
	read Y: 		number,
	read Threads: 	number,
	read Resample: 	string,
	RenderNum:		number?
}

type pIn = {[number]: any}

local S: any = nil
local P: any = nil
local VM = require( `./plugin/process` )
local display = require( `./plugin/display`)

-- A Pajamas project assumes that data passed to it will always be a strings.
-- This function performs type coercion so that data can be matched with the .pajamasrc module.
local function parse( str: string ): any
	if str == "true" or str == "false" then
		return str == "true"
	end
	return tonumber(str) or str
end

-- Cross-checks launch parameters with Penumbra's .pajamasrc module.
-- Configures & returns settings and launch parameters to the module function.
-- This function is only called once in a given VM.
local function Init(Input: pIn): (self, params)
	local Params, Misc = 		VM.Launch_Parse(Input, require(`@self/.pajamasrc`).params :: any)
	local S_New: any = 			{}
	S_New.Display_Res = 		display.SanitiseResolution( vector.create(Params.X, Params.Y) )
	S_New.Render_Threads = 		math.max(Params.Threads, 1) // 1
	S_New.Display_Resample = 	if ( Params.Resample :: any ) == true then "Bilinear" else "Nearest"
	S_New.Display_Res //= 		1
	S_New.Display_Res = 		vector.create((S_New.Display_Res.x//2)*2,S_New.Display_Res.y)
	S_New.Display_Res =			vector.clamp(
		vector.create(S_New.Display_Res.x, (S_New.Display_Res.y // S_New.Render_Threads) * S_New.Render_Threads),
		vector.create(1, 1), vector.create(1024, 1024)
	)
	S_New.Actor_Res = 			vector.create(S_New.Display_Res.x, S_New.Display_Res.y // S_New.Render_Threads)
	VM.Launch_Init(Misc.Host)
	S_New.Actor_RenderNum = 	Params.RenderNum
	S = 						S_New
	P = 						Params
	-- Run module called via T.Path
	local M = 					require( `@self/{Params.Path}` )() :: any
	-- Return settings and launch parameters
	return S :: self, P :: params
end

-- Perform init or configured settings if already generated
return function(...): (self, params)
	local x = ...
	if (not x) or (S and P) then
		return S :: self, P :: params
	end
	return Init({...} :: pIn)
end

--[[ Resolution : Pixel Count
	16:9
160x90 : 14400,		177x100 : 17700, 	192x108 : 20736, 	213x120 : 25560, 	256x144 : 36720, 	288x162 : 46656, 	320x180 : 57600,
384x216 : 82944, 	426x240 : 102240,	470x264 : 124080, 	480x270 : 129600, 	512x288 : 147456, 	533x300 : 159900,
568x320 : 181760,	597x336 : 200592,	640x360 : 230400, 	675x380 : 256500, 	683x384 : 262272, 	711x400 : 284400,
725x408 : 295800, 	747x420 : 313740, 	768x432 : 331776, 	800x450 : 360000, 	854x480 : 409920, 	910x512 : 465920
960x540 : 518400, 	1024x576 : 589824

	4:3
192x144 : 27648, 	240x180 : 43200, 	320x240 : 76800, 	360x270 : 97200, 	384x288 : 110592, 	416x312 : 129792,
427x320 : 136640, 	448x336 : 150528, 	480x360 : 172800, 	512x384 : 196608, 	560x420 : 235200, 	640x480 : 307200,
720x540 : 388800, 	768x576 : 442368, 	800x600 : 480000, 	1024x768 : 786432

	RETRO:
[Gameboy] 	160x144 : 23040
[GBA]		240x160 : 28400
[SNES]		256x224 : 57344
[NES]		256x240 : 61440
[Genesis]	320x240 : 76800
]]


--!strict
--!native
--Deepfreeze: Freezes table and all child tables, prevents writes & improves read speeds when in tight loops
local function Deepfreeze<T>( t: T ): T
	if type( t ) ~= "table" then
		return t
	end
	local tab: {[any]: any} = t :: any
	for _, v in tab do
		-- Prevents cyclic tables from looping infinitely
		if type( v ) == "table" and not table.isfrozen( v ) then
			Deepfreeze( v )
		end
	end
	table.freeze( tab )
	return tab
end

return Deepfreeze


--[[
	Penumbra: Shader module builder [Roblox]

	This module allows you to build shader modules in Roblox Studio.

	This works by inlining 'template/prepend.luau' and 'template/append.luau',
	with your own provided shader modulein the middle.
	If a shader module isn't provided, then 'template/shader.luau' module will be used.
	'--!strict' and '--!native' will be inlined by default.

	If you want to take advantage of this module builder, call the function
	with your shader module as the given input.

	The system will look for all occurences of '--/INLINE', and copy the
	relevant source into the module you are building.

	This should allow for quicker iteration on the creation of a shader module
	which is compatible with Penumbra.

	If you're developing outside of Roblox, it might be better for you
	to write your own shell script to do this.
]]

local function BuildShader(in_prepend: ModuleScript, in_shader: ModuleScript, in_append: ModuleScript): ModuleScript
	-- Get template modules
	local scripts: {ModuleScript} = {
		in_prepend or script.template.frag.prepend,
		in_shader or script.template.frag.shader,
		in_append or script.template.frag.append
	}
	local outputSource: string = `--!strict\n--!native\n`
	for _, module: ModuleScript in scripts do
		local source: string = module.Source
		local splitChar: {string} = string.split( source, `--/INLINE\n` )
		table.remove( splitChar, 1 )
		for i = 1, #splitChar do
			outputSource ..= splitChar[i]..`\n`
		end
		print(`Inlined '{module.Name}'`)
	end
	local outputModule = Instance.new("ModuleScript")
	outputModule.Source = outputSource
	outputModule.Name = in_shader.Name
	print("Built inlined module:", outputModule)
	return outputModule
end

local Build = {}

function Build.Directory(dir, output)
	dir = dir or script.Parent.shader_src
	local dirs = dir:GetChildren()
	for _, d in dirs do
		local shader_type = d.Name
		local modules = d:GetChildren()
		for _, m in modules do
			if m:IsA("ModuleScript") then
				BuildShader(
					script.template[shader_type].prepend,
					m,
					script.template[shader_type].append
				).Parent = ( output and output[shader_type] or script.parent.shaders[shader_type] )
			end
		end
	end
end

function Build.OnceFromSource(input: ModuleScript, output: Instance?): ModuleScript
	BuildShader(input).Parent = output or script.parent.shaders
end

return Build


--!strict
--!native
-- lRGB - Piecewise function
-- Separate shader function for legacy purposes.
-- Pixel shader: sRGB -> lRGB color space transform
-- Input is assumed to already be bound 0-1 sRGB

local sRGB_Sub: vector =			vector.one * 0.055
local vector_zero =					vector.zero
local vector_one =					vector.one

return function( iTime: number, In: {vector}, Out: {vector} )
	for i, RGB in In do
		-- Gamma Sign - Branchless, vector.sign acts as per-axis boolean for writing flat/gamma to x/y/z
		local Gamma_Sign: vector = vector.sign( RGB // 0.04045 )
		-- Output
		local Gamma_RGB: vector = (RGB+sRGB_Sub)/1.055
		Out[i] =
			-- Flat portion
			( vector_one - Gamma_Sign ) * (RGB/12.92) +
			-- Gamma portion
			Gamma_Sign * vector.create(
					Gamma_RGB.x ^ 2.4,
					Gamma_RGB.y ^ 2.4,
					Gamma_RGB.z ^ 2.4
				)
	end
end


--!strict
--!native
-- sRGB - Fast Approximation
local v_1: vector =			vector.one * 1
local v_0_625: vector =		vector.one * 0.625
local Reciprocal: vector =	vector.one / 4.8
return function( iTime: number, In: {vector}, Out: {vector} )
	for c = 1, #In do
		Out[c] =
			( v_1 - In[c] ) * ( In[c] / ( In[c] + Reciprocal ) )
			+ In[c] * ( v_0_625 + In[c] * 0.375 )
	end
end


--!strict
--!native
-- sRGB - Fastest Approximation - Highly inaccurate, performance tradeoff not really worth it
local v_1: vector =		vector.one
local v_0_225: vector =	vector.one * 0.225
return function( iTime: number, In: {vector}, Out: {vector} )
	for c = 1, #In do
		Out[c] = ( In[c] * ( In[c] * .225 + v_1 ) ) / ( In[c] + v_0_225 )
	end
end


--!strict
--!native
-- sRGB Pow approximation
local vector_zero: vector = vector.zero
local vector_one: vector = vector.one
return function( iTime: number, In: {vector}, Out: {vector} )
	for c: number, RGB: vector in In do
		Out[c] = vector.create(RGB.x^.4545,RGB.y^.4545,RGB.z^.4545)
	end
end


--!strict
--!native
-- sRGB Tanh approximation
local vector_zero: vector = vector.zero
return function( iTime: number, In: {vector}, Out: {vector} )
	for c: number, RGB: vector in In do
		Out[c] = vector.create(math.tanh(RGB.x),math.tanh(RGB.y),math.tanh(RGB.z))
	end
end


--!strict
--!native
-- sRGB - Piecewise function
-- Separate shader function for legacy purposes.
-- Pixel shader: Linear -> sRGB color space transform
-- Input is assumed to already be bound 0-1 linear RGB

-- Bypass (number) -> (vector) broadcast performance hit
local sRGB_Sub: vector =			vector.one * 0.055
local vector_zero =					vector.zero
local vector_one =					vector.one

return function( iTime: number, In: {vector}, Out: {vector} ) -- Optimized version
	-- SIMD Linear RGB -> sRGB conversion
	for i, RGB in In do
		-- Gamma Sign - Branchless, vector.sign acts as per-axis boolean for writing flat/gamma to x/y/z
		local Gamma_Sign: vector = vector.sign( RGB // 0.0031308 )
		-- Output
		Out[i] =
			-- sRGB: Flat portion
			( vector_one - Gamma_Sign ) * RGB * 12.92 +
			-- sRGB: Gamma portion
			Gamma_Sign * (
				1.055 * vector.create(
					RGB.x ^ 0.4166666666666667,
					RGB.y ^ 0.4166666666666667,
					RGB.z ^ 0.4166666666666667
				) - sRGB_Sub
			)
	end
end


--!strict
--!native
-- Tonemapper: AcesFilm [Source: https://www.shadertoy.com/view/W3lfRN]
local v_1: vector =			vector.one * 1
local v_0: vector =			vector.zero
local v_0_625: vector =		vector.one * 0.625
local v_0_003: vector =		vector.one * 0.003
local v_0_59: vector =		vector.one * 0.59
local v_0_14: vector =		vector.one * 0.14
local Reciprocal: vector =	vector.one / 4.8
local function Shader( x: vector ): vector
	return vector.clamp( ( x * ( 2.51 * x + v_0_003 ) ) / ( x * ( 2.43 * x + v_0_59 ) + v_0_14 ), v_0, v_1 )
end
return function( iTime: number, In: {vector}, Out: {vector} )
	for c = 1, #In do Out[c] = Shader( In[c] ) end
end


--!strict
--!native
-- Tonemapper: AgX [Source: https://www.shadertoy.com/view/dtyfRw]
local Settings =	require( "../../../Settings" )
local MinEV: number = -10 -- Tonemapper min. exposure
local MaxEV: number = 7.5 -- Tonemapper max. exposure

local To_AgX = {
	vector.create(.842479062253094,.0423282422610123,.0423756549057051),
	vector.create(.0784335999999992,.878468636469772,.0784336),
	vector.create(.0792237451477643,.0791661274605434,.879142973793104)
}

local From_AgX = {
	vector.create(1.19687900512017,-0.0528968517574562,-0.0529716355144438),
	vector.create(-0.0980208811401368,1.15190312990417,-0.0980434501171241),
	vector.create(-0.0990297440797205,-0.0989611768448433,1.15107367264116)
}

table.freeze( To_AgX )
table.freeze( From_AgX )

local function v3_1( n: number ): vector return vector.create( n, n, n ) end

return function( iTime: number, In: {vector}, Out: {vector} )
	local x2: vector, x4: vector
	local vC_1 = v3_1(15.5)
	local vC_2 = v3_1(40.14)
	local vC_3 = v3_1(31.96)
	local vC_4 = v3_1(6.868)
	local vC_5 = v3_1(0.4298)
	local vC_6 = v3_1(0.1191)
	local vC_7 = v3_1(0.00232)
	local vector_zero, vector_one = vector.zero, vector.one
	local minEv: vector = vector.one * MinEV
	local maxEv: vector = vector.one * MaxEV
	local divEv_Reciprocal: vector = 1 / ( maxEv - minEv )
	local To_AgX_1 = To_AgX[1]
	local To_AgX_2 = To_AgX[2]
	local To_AgX_3 = To_AgX[3]
	local From_AgX_1 = From_AgX[1]
	local From_AgX_2 = From_AgX[2]
	local From_AgX_3 = From_AgX[3]
	for c: number = 1, #In do
		local x: vector = In[c]
		x = vector.clamp(
			vector.create(
				math.log( vector.dot( To_AgX_1, x ), 2 ),
				math.log( vector.dot( To_AgX_2, x ), 2 ),
				math.log( vector.dot( To_AgX_3, x ), 2 )
			),
			minEv, maxEv
		)
		x = ( x - minEv ) * divEv_Reciprocal
		local x2: vector = x * x
		local x4: vector = x2 * x2
		x = x4 * x2 *	15.5 -
			x4 * x *	40.14 +
			x4 *		31.96 -
			x2 * x *	6.868 +
			x2 *		0.4298 +
			x *			0.1191 -
			vC_7
		-- AgX -> lRGB Matrix3 Transform [EOTF] -> Clamp 0-1 -> Write changes
		Out[c] = vector.clamp(
			vector.create(
				vector.dot( From_AgX_1, x ),
				vector.dot( From_AgX_2, x ),
				vector.dot( From_AgX_3, x )
			),
			vector_zero,
			vector_one
		)
	end
end


--!strict
--!native
-- Penumbra: Fragment Shader - CRT [Source: Crazyblox]
type f64=number; type vec=vector
local S, Textures = require( "../../../Settings" ), require( "../../../Textures" )
local vec =	vector.create;local vec_1, vec_0_5, vec_0 = vec(1,1,1), vec(.5,.5,.5), vec(0,0);local iResolution:vec =vec(S.Display_Res.x,S.Display_Res.y);local iResolution_div:vec = iResolution+vec(0,0,1);
local radians, degrees, sin, cos, tan, asin, acos, atan, sinh, cosh, tanh, pow, exp, log, exp2, log2, sqrt, inversesqrt, abs, sign, floor, ceil, fract, mod, modf, min, max, clamp, mix, step, smoothstep, length, distance, dot, cross, normalize = -- Requiring implementation: asinh, acosh, atanh, exp2, inversesqrt, trunc, step, smoothstep, distance, faceforward, reflect, refract, determinant, outerProduct, matrixCompMult, inverse, transpose
	math.rad, math.deg, math.sin, math.cos, math.tan, math.asin, math.acos, math.atan, math.sinh, math.cosh, math.tanh, math.pow, math.exp, math.log, math.exp, function( n:f64 ):f64 return math.log( n, 2. ) end, math.sqrt, math.sqrt, math.abs, math.sign, math.floor, math.ceil, function( n:f64 ):f64 return n - math.floor(n) end, function( n:f64, m:f64 ):f64 return n % m end, math.modf, math.min, math.max, math.clamp, math.lerp, function( edge:f64, x:f64 ):f64 return x end, function(e0:f64,e1:f64,x:f64):f64 local t:f64=math.clamp((x-e0)/(e1-e0),0.,1.) return t*t*(3-2*t) end, vector.magnitude, vector.magnitude, vector.dot, vector.cross, vector.normalize
local function Sampler_Nearest_Repeat(t:Textures.Texture,UV:vec):vec UV=vec(UV.x%1,UV.y%1)//t.FD;return t[ UV.x + UV.y * t.Width ]end;local function Sampler_Nearest_Clamp(t:Textures.Texture,UV:vec):vec UV=vector.clamp(UV//t.FD,vec_0,vec(t.Width0,t.Height0)); return t[UV.x+UV.y*t.Width];end;local function Sampler_Linear_Repeat(t:Textures.Texture,UV:vec):vec UV*=vec(t.Width,t.Height);UV-=vec(.5,.5);local UV_0:vec =vector.floor(UV);local UV_1:vec =vector.ceil(UV);local A:vec =UV-UV_0;local X0:f64 =UV_0.x%t.Width;local X1:f64 =UV_1.x%t.Width;local Y0:f64 =(UV_0.y%t.Height)*t.Width;local Y1:f64 =(UV_1.y%t.Height)*t.Width;return vector.lerp(vector.lerp(t[X0+Y0],t[X1+Y0],A.x),vector.lerp(t[X0+Y1],t[X1+Y1],A.x),A.y);end;local function Sampler_Linear_Clamp( t:Textures.Texture,UV:vec ):vec UV*=vec(t.Width,t.Height);UV-=vec(.5,.5);local UV_0:vec =vector.floor(UV);local UV_1:vec =vector.ceil(UV);UV_0=vector.clamp(UV_0,vec_0,vec(t.Width0,t.Height0));UV_1=vector.clamp(UV_1,vec_0,vec(t.Width0,t.Height0));local A:vec =UV-UV_0;local X0:f64 =UV_0.x%t.Width;local X1:f64 =UV_1.x%t.Width;local Y0:f64 =(UV_0.y%t.Height)*t.Width;local Y1:f64 =(UV_1.y%t.Height)*t.Width;return vector.lerp(vector.lerp(t[X0+Y0],t[X1+Y0],A.x),vector.lerp(t[X0+Y1],t[X1+Y1],A.x),A.y)end
--[[ SHADER ]]--
local texture = Sampler_Linear_Repeat
local function Shader( fragColor:vec, fragCoord:vec, iTime:f64 ):vec
	fragColor *=
		-- Tanh Scanline [3x samples, *3 raise before tanh]
		mix((tanh(3.*abs(sin(fragCoord.y-.33)))+tanh(3.*abs(sin(fragCoord.y)))+tanh(3.*abs(sin(fragCoord.y+.33))))*.3333,1.,.5)
		-- Vignette
		* max(1.-(length(fragCoord-(iResolution*.5))/(max(iResolution.x,iResolution.y))),0.)
		-- 2x mult for before tanh tonemap
		* 2.
	return vec(tanh(fragColor.x),tanh(fragColor.y),tanh(fragColor.z))

end
--[[ MODULE/WRAPPER: Must be placed at bottom of module ]]--
local iResolution0:vec =iResolution-vec(1,1);local iResolution0_div:vec =iResolution0+vec(0,0,1);local tStart0:vec =vec(0,S.Actor_Res.y*(S.Actor_RenderNum-1));local tRes1:vec =S.Actor_Res;local tRes0:vec =S.Actor_Res-vec(1,1);local tEnd0:vec =tStart0+tRes0;local tScale:f64 =1;local C_Start:f64 =1+(((tStart0.y//tScale*tScale)-tStart0.y)*iResolution.x)
return function(T:f64,I:{vec},O:{vec})local c:f64 =C_Start;local i_Res0_y:f64 =iResolution0.y;for Y:f64 =tStart0.y//tScale*tScale,tEnd0.y,tScale do for X:f64 =tStart0.x,tEnd0.x,tScale do O[c]=Shader(I[c],vec(X,i_Res0_y-Y),T);c+=tScale;end;c+=iResolution.x*(tScale-1);end if tScale<=1 then return end;c=C_Start;for Y:f64 =0,tRes0.y do for X:f64 =tStart0.x,tEnd0.x do local uv:vec =vec(X,Y)//tScale*tScale;O[c]=O[uv.x+uv.y*iResolution.x+1];c+=1;end;end;end


--!strict
--!native
local vector_zero: vector = vector.zero
local vector_one: vector = vector.one
return function( iTime: number, In: {vector}, Out: {vector} )
	for c = 1, #In do
		Out[c] = vector.clamp( In[c], vector_zero, vector_one )
	end
end


--!strict
--!native
-- Tonemapper: Reinhard RGB
local Burn: vector = vector.one
return function( iTime: number, In: {vector}, Out: {vector} )
	for c: number = 1, #In do
		Out[c] = In[c] / ( Burn + In[c] )
	end
end


--!nocheck
--!nolint
--/INLINE
local iRes0:vec =	iRes-vec(1,1)
local iRes0_div:vec=iRes0+vec(0,0,1)
local tStart0:vec =	vec(0,S.Actor_Res.y*(S.Actor_RenderNum-1))
local tRes1:vec =	S.Actor_Res
local tRes0:vec =	S.Actor_Res-vec(1,1)
local tEnd0:vec =	tStart0+tRes0
local IL_Start:f64 = tStart0.y % 2

-- Interlace data & function
local IL = { x = { ext = {tStart0.y % 2, 2}, int = {0, 2} }, y = { ext = {0, 2}, int = {0, 1} } }
local function InterlaceStep(): (f64, f64, f64, f64)
	-- X: Internal clock, external clock
	IL.x.int[1] += 1; IL.x.int[1] %= IL.x.int[2]
	IL.x.ext[1] += ( IL.x.int[1] == 0 ) and 1 or 0; IL.x.ext[1] %= IL.x.ext[2]
	-- Y: Internal clock, external clock
	IL.y.int[1] += 1; IL.y.int[1] %= IL.y.int[2]
	IL.y.ext[1] += ( IL.y.int[1] == 0 ) and 1 or 0; IL.y.ext[1] %= IL.y.ext[2]
	-- Return XY offsets, XY steps
	Sampler_IL_Offset = vec( -.333 + IL.y.ext[1] * .666,  -.333 + IL.x.ext[1] * .666 )
	return IL.x.ext[1], IL.y.ext[1], IL.x.ext[2], IL.y.ext[2]
end

for i = 1, IL_Start do
	InterlaceStep()
end

local function ShaderCall( T: f64, FB_Slice_In: buffer, FB_Slice_Out: buffer, c: f64, X_i: f64, Y_i: f64 )
	--vec3_buffer( Out, c, main( buffer_vec3( In, c ), vec( X_i, Y_i ), T ) )
	-- ^^^ For inputting previous shader as fragColor
	vec3_buffer( FB_Slice_Out, c, main( vec_0, vec( X_i, Y_i ), T ) )
	-- ^^^ For inputting blank white (More performance)
end

return function( T: f64, FB_Slice_In: buffer, FB_Slice_Out: buffer, InterlaceOn: boolean, FB_Full_New: Textures.Texture )
	FB_Full = FB_Full_New
	T = preprocess(T)
	-- Set steps & offsets
	local X_Off:f64, Y_Off:f64, X_Jump:f64, Y_Jump:f64 = 0, 0, 1, 1
	--if InterlaceOn then X_Off, Y_Off, X_Jump, Y_Jump = InterlaceStep() end
	-- Set XY byte increments
	local X_Jump_Bytes:f64 = u32B
	local Y_Jump_Bytes:f64 = iRes.x * u32B
	-- Set XY byte start offsets byte offsets
	local X_Off_Bytes:f64 = X_Jump_Bytes * X_Off
	local Y_Off_Bytes:f64 = Y_Jump_Bytes * Y_Off
	local c:f64 = X_Off_Bytes + Y_Off_Bytes
	-- X_Jump_Bytes always needs to increment; our 'c' cursor depends on this.
	X_Jump_Bytes += X_Jump_Bytes * (X_Jump-1)
	Y_Jump_Bytes *= (Y_Jump-1)
	for Y_i:f64 = tStart0.y + Y_Off, tEnd0.y, Y_Jump do
		--for Y_i:f64 = iRes0.y - tStart0.y - Y_Off, iRes0.y - tEnd0.y, -Y_Jump do
		for X_i:f64 = tStart0.x + X_Off, tEnd0.x, X_Jump do
			ShaderCall( T, FB_Slice_In, FB_Slice_Out, c, X_i, Y_i )
			c += X_Jump_Bytes
		end
		c += Y_Jump_Bytes
	end
end


--!nocheck
--!nolint
--/INLINE
type f64 = number; type vec = vector
local S = require( `../../../../` )()
local Textures = require( `../../../textures` )
local vec =	vector.create
local vec_1, vec_0_5, vec_0 = vec( 1, 1, 1 ), vec( .5, .5, .5 ), vec( 0, 0 )
local iRes:vec = vec( S.Display_Res.x, S.Display_Res.y )
local iRes_div:vec = iRes + vec( 0, 0, 1 )
local Sampler_IL_Offset:vec
local FB_Full: Textures.Texture
local DoInterlace: boolean = true

-- RGBAu8/vec3 buffer read/write functions
local u32B: number = 4
local buffer_vec3 = function(b: buffer ,i: number ): vector
	local n:number = buffer.readu32(b,i)
	return vector.create(bit32.band(n,0x000000FF),bit32.rshift(bit32.band(n,0x0000FF00),8),bit32.rshift(bit32.band(n,0x00FF0000),16))*.00392156862745098
end
local vec3_buffer = function(b: buffer, i: number, v: vector)
	v*=0xFF
	buffer.writeu32(b,i,v.x+bit32.lshift(v.y,8)+bit32.lshift(v.z,16)+0xFF000000)
end

-- Texture sampling functions
local function Sampler_Nearest(t: Textures.Texture, UV: vector ): vector
	local tW: number, tH: number = t.Width, t.Height
	UV *= vector.create( tW, tH )			-- Expand resolution
	UV //= 1								-- Set top-left UV integer
	local x:number	= UV.x % tW				-- Wrap U to texture width
	local y:number	= ( UV.y % tH ) * tW	-- Wrap V to texture height
	local i:number	= (x + y) * u32B		-- Set index value
	return buffer_vec3(t.Buffer, i)			-- Return read value
end
-- Texture sampling functions
local function Sampler_Nearest_IL(t: Textures.Texture, UV: vector ): vector
	local tW: number, tH: number = t.Width, t.Height
	UV *= vector.create( tW, tH )			-- Expand resolution
	UV += Sampler_IL_Offset					-- Add interlace jitter
	UV //= 1								-- Set top-left UV integer
	local x:number	= UV.x % tW				-- Wrap U to texture width
	local y:number	= ( UV.y % tH ) * tW	-- Wrap V to texture height
	local i:number	= (x + y) * u32B		-- Set index value
	return buffer_vec3(t.Buffer, i)			-- Return read value
end
local function Sampler_Linear( t: Textures.Texture, UV: vector ): vector
	local tW: number, tH: number = t.Width, t.Height
	UV *=				vector.create(tW, tH)		-- Expand resolution
	UV -=				vector.create( .5, .5 )		-- Shifts texture origin from top-left to middle of pixel
	local UV_0:vector =	UV // 1						-- Set top-left UV integer
	local UV_1:vector =	UV_0 + vec_1				-- Set bottom-right UV integer
	local A:vector =	UV - UV_0					-- Fetch XY lerp alpha
	local S0:number =	UV_0.x % tW					-- Fetch X0 index
	local S2:number =	UV_1.x % tW					-- Fetch X1 index
	local S1:number =	( UV_0.y % tH ) * tW		-- Fetch Y0 index
	local S3:number =	( UV_1.y % tH ) * tW		-- Fetch Y1 index
	local b:buffer =	t.Buffer					-- Fetch texture buffer
	S0, S1, S2, S3 = S0+S1, S2+S1, S0+S3, S2+S3		-- Calculate sample indexes
	S0 *= u32B; S1 *= u32B; S2 *= u32B; S3 *= u32B	-- Multiply sample indexes
	return vector.lerp(
		vector.lerp( buffer_vec3( b, S0 ), buffer_vec3( b, S1 ), A.x ),	-- Lerp X0Y0 -> X1Y0
		vector.lerp( buffer_vec3( b, S2 ), buffer_vec3( b, S3 ), A.x ),	-- Lerp X0Y1 -> X1Y1
		A.y																-- Lerp rows Y0 and Y1
	)
end
--[[
-- 2x2 Box Blur
local function Sampler_BoxBlur(t: Textures.Texture, UV: vector): vector
	local tW:number, tH:number = t.Width, t.Height
	UV = (UV * vector.create(tW,tH) + vector.create(.5,.5) ) // 1
	local x0:number	= UV.x % tW 			* u32B	-- Wrap U to texture width
	local y0:number	= ( UV.y % tH ) * tW 	* u32B	-- Wrap V to texture height
	UV += vec_1										-- Get bottom-right
	local x1:number	= UV.x % tW 			* u32B	-- Wrap U to texture width
	local y1:number	= ( UV.y % tH ) * tW 	* u32B	-- Wrap V to texture height
	return (
		buffer_vec3(t.Buffer, x0+y0) +				-- Sample top-left
		buffer_vec3(t.Buffer, x1+y0) +				-- Sample top-right
		buffer_vec3(t.Buffer, x0+y1) +				-- Sample bottom-left
		buffer_vec3(t.Buffer, x1+y1)				-- Sample bottom-right
	) * .25
end
]]
--[[
-- 3x3 Gaussian Unsharp Mask
local function Sampler_Unsharp(t: Textures.Texture, UV: vector): vector
	local b = t.Buffer
	local tW:number, tH:number = t.Width, t.Height
	UV = (UV * vector.create(tW,tH) + vector.create(.5,.5) ) // 1
	-- X1Y1
	local x:number	= UV.x % tW 			* u32B	-- Wrap U to texture width
	local y:number	= ( UV.y % tH ) * tW	* u32B	-- Wrap V to texture height
	local OG:vector = buffer_vec3(b, x+y)
	local S:vector = OG * 13
	-- X0Y0
	UV += vector.create(-1, -1)
	x = UV.x % tW 			* u32B	-- Wrap U to texture width
	y = ( UV.y % tH ) * tW 	* u32B	-- Wrap V to texture height
	S -= buffer_vec3(b, x+y)
	-- X1Y0
	UV += vector.create(1, 0)
	x = UV.x % tW 			* u32B	-- Wrap U to texture width
	y = ( UV.y % tH ) * tW 	* u32B	-- Wrap V to texture height
	S -= buffer_vec3(b, x+y)* 2
	-- X2Y0
	UV += vector.create(1, 0)
	x = UV.x % tW 			* u32B	-- Wrap U to texture width
	y = ( UV.y % tH ) * tW 	* u32B	-- Wrap V to texture height
	S -= buffer_vec3(b, x+y)
	-- X0Y1
	UV += vector.create(-2, 1)
	x = UV.x % tW 			* u32B	-- Wrap U to texture width
	y = ( UV.y % tH ) * tW 	* u32B	-- Wrap V to texture height
	S -= buffer_vec3(b, x+y)* 2
	-- X0Y1
	UV += vector.create(2, 0)
	x = UV.x % tW 			* u32B	-- Wrap U to texture width
	y = ( UV.y % tH ) * tW 	* u32B	-- Wrap V to texture height
	S -= buffer_vec3(b, x+y)* 2
	-- X0Y2
	UV += vector.create(-2, 1)
	x = UV.x % tW 			* u32B	-- Wrap U to texture width
	y = ( UV.y % tH ) * tW 	* u32B	-- Wrap V to texture height
	S -= buffer_vec3(b, x+y)
	-- X1Y2
	UV += vector.create(1, 0)
	x = UV.x % tW 			* u32B	-- Wrap U to texture width
	y = ( UV.y % tH ) * tW 	* u32B	-- Wrap V to texture height
	S -= buffer_vec3(b, x+y)* 2
	-- X2Y2
	UV += vector.create(1, 0)
	x = UV.x % tW 			* u32B	-- Wrap U to texture width
	y = ( UV.y % tH ) * tW 	* u32B	-- Wrap V to texture height
	S -= buffer_vec3(b, x+y)
	-- Return
	return vector.lerp( OG, vector.clamp( S, vec_0, vec_1 ), .2 )
end
]]

-- Math aliases
local radians, degrees, sin, cos, tan, asin, acos, atan, sinh, cosh, tanh, pow, exp, log, exp2, log2, sqrt, inversesqrt, abs, sign, floor, ceil, fract, mod, modf, min, max, clamp, mix, step, smoothstep, length, distance, dot, cross, normalize = -- Requiring implementation: asinh, acosh, atanh, exp2, inversesqrt, trunc, step, smoothstep, distance, faceforward, reflect, refract, determinant, outerProduct, matrixCompMult, inverse, transpose
	math.rad, math.deg, math.sin, math.cos, math.tan, math.asin, math.acos, math.atan, math.sinh, math.cosh, math.tanh, math.pow, math.exp, math.log, math.exp, function( n:f64 ):f64 return math.log( n, 2. ) end, math.sqrt, math.sqrt, math.abs, math.sign, math.floor, math.ceil, function( n:f64 ):f64 return n - math.floor(n) end, function( n:f64, m:f64 ):f64 return n % m end, math.modf, math.min, math.max, math.clamp, math.lerp, function( edge:f64, x:f64 ):f64 return x end, function(e0:f64,e1:f64,x:f64):f64 local t:f64=math.clamp((x-e0)/(e1-e0),0.,1.) return t*t*(3-2*t) end, vector.magnitude, vector.magnitude, vector.dot, vector.cross, vector.normalize


--!nocheck
--!nolint
--/INLINE
local iChannel0: Textures.Texture = Textures.Load.Base( {}, "rustymetal" )
local texture = Sampler_Nearest -- Sampler_Nearest_IL, Sampler_Linear, Sampler_BoxBlur, Sampler_Unsharp

local function main( fragColor:vec, fragCoord:vec, iTime:f64 ):vec
	-- Gradient - Sourced from [https://www.shadertoy.com/new]
	local grad:vec = ( fragCoord / iResolution ) + vec_1 * iTime
	grad = vec(cos(grad.x),cos(grad.y+2.),cos(grad.x+4.)) * .5 + vec_0_5
	-- Sample texture
	local uv:vec = ( fragCoord + vec(.5,.5) ) / iResolution
	local sample:vec = texture( iChannel0, uv + vec( sin(iTime) * .5, cos(iTime) * .5 ) )
	-- Return gradient-multiplied sample
	return sample * grad
end

local function preprocess( iTime:f64 ): f64
	return iTime
end


--!nocheck
--!nolint
--/INLINE
local iRes0:vec =	iRes-vec(1,1)
local iRes0_div:vec =iRes0+vec(0,0,1)
local tStart0:vec =	vec(0,S.Actor_Res.y*(S.Actor_RenderNum-1))
local tRes1:vec =	S.Actor_Res
local tRes0:vec =	S.Actor_Res-vec(1,1)
local tEnd0:vec =	tStart0+tRes0
local IL_Start:f64 = tStart0.y % 2

-- Interlace data & function
local IL = { x = { ext = {tStart0.y % 2, 2}, int = {0, 2} }, y = { ext = {0, 2}, int = {0, 1} } }
local function InterlaceStep(): (f64, f64, f64, f64)
	-- X: Internal clock, external clock
	IL.x.int[1] += 1; IL.x.int[1] %= IL.x.int[2]
	IL.x.ext[1] += ( IL.x.int[1] == 0 ) and 1 or 0; IL.x.ext[1] %= IL.x.ext[2]
	-- Y: Internal clock, external clock
	IL.y.int[1] += 1; IL.y.int[1] %= IL.y.int[2]
	IL.y.ext[1] += ( IL.y.int[1] == 0 ) and 1 or 0; IL.y.ext[1] %= IL.y.ext[2]
	-- Return XY offsets, XY steps
	Sampler_IL_Offset = vec( -.333 + IL.y.ext[1] * .666,  -.333 + IL.x.ext[1] * .666 )
	return IL.x.ext[1], IL.y.ext[1], IL.x.ext[2], IL.y.ext[2]
end

for i = 1, IL_Start do
	InterlaceStep()
end

local function ShaderCall( T: f64, FB_Slice_In: buffer, FB_Slice_Out: buffer, c: f64, X_i: f64, Y_i: f64 )
	--vec3_buffer( FB_Slice_Out, c, main( buffer_vec3( FB_Slice_In, c ), vec( X_i, Y_i ), T ) )
	-- ^^^ For inputting previous shader as fragColor
	vec3_buffer( FB_Slice_Out, c, main( vec_0, vec( X_i, Y_i ), T ) )
	-- ^^^ For inputting blank white (More performance)
end

return function( T: f64, FB_Slice_In: buffer, FB_Slice_Out: buffer, InterlaceOn: boolean, FB_Full_New: Textures.Texture )
	FB_Full = FB_Full_New
	T = preprocess(T)
	-- Set steps & offsets
	local X_Off:f64, Y_Off:f64, X_Jump:f64, Y_Jump:f64 = 0, 0, 1, 1
	if InterlaceOn and DoInterlace then
		X_Off, Y_Off, X_Jump, Y_Jump = InterlaceStep()
	end
	-- Set XY byte increments
	local X_Jump_Bytes:f64 = u32B
	local Y_Jump_Bytes:f64 = iRes.x * u32B
	-- Set XY byte start offsets byte offsets
	local X_Off_Bytes:f64 = X_Jump_Bytes * X_Off
	local Y_Off_Bytes:f64 = Y_Jump_Bytes * Y_Off
	local c:f64 = X_Off_Bytes + Y_Off_Bytes
	-- X_Jump_Bytes always needs to increment; our 'c' cursor depends on this.
	X_Jump_Bytes += X_Jump_Bytes * (X_Jump-1)
	Y_Jump_Bytes *= (Y_Jump-1)
	for Y_i:f64 = iRes0.y - tStart0.y - Y_Off, iRes0.y - tEnd0.y, -Y_Jump do
		for X_i:f64 = tStart0.x + X_Off, tEnd0.x, X_Jump do
			ShaderCall( T, FB_Slice_In, FB_Slice_Out, c, X_i, Y_i )
			c += X_Jump_Bytes
		end
		c += Y_Jump_Bytes
	end
end


--!nocheck
--!nolint
--/INLINE
type f64 = number; type vec = vector
local S = require( `../../../../` )()
local Textures = require( `../../../textures` )
local vec =	vector.create
local vec_1, vec_0_5, vec_0 = vec( 1, 1, 1 ), vec( .5, .5, .5 ), vec( 0, 0 )
local iRes:vec = vec( S.Display_Res.x, S.Display_Res.y )
local iRes_div:vec = iRes + vec( 0, 0, 1 )
local Sampler_IL_Offset:vec
local FB_Full: Textures.Texture
local DoInterlace: boolean = true

-- RGBAu8/vec3 buffer read/write functions
local u32B: number = 4
local buffer_vec3 = function(b: buffer ,i: number ): vector
	local n:number = buffer.readu32(b,i)
	return vector.create(bit32.band(n,0x000000FF),bit32.rshift(bit32.band(n,0x0000FF00),8),bit32.rshift(bit32.band(n,0x00FF0000),16))*.00392156862745098
end
local vec3_buffer = function(b: buffer, i: number, v: vector)
	v*=0xFF
	buffer.writeu32(b,i,v.x+bit32.lshift(v.y,8)+bit32.lshift(v.z,16)+0xFF000000)
end

-- Texture sampling functions
local function Sampler_Nearest(t: Textures.Texture, UV: vector ): vector
	local tW: number, tH: number = t.Width, t.Height
	UV *= vector.create( tW, tH )			-- Expand resolution
	UV //= 1								-- Set top-left UV integer
	local x:number	= UV.x % tW				-- Wrap U to texture width
	local y:number	= ( UV.y % tH ) * tW	-- Wrap V to texture height
	local i:number	= (x + y) * u32B		-- Set index value
	return buffer_vec3(t.Buffer, i)			-- Return read value
end
-- Texture sampling functions
local function Sampler_Nearest_IL(t: Textures.Texture, UV: vector ): vector
	local tW: number, tH: number = t.Width, t.Height
	UV *= vector.create( tW, tH )			-- Expand resolution
	UV += Sampler_IL_Offset					-- Add interlace jitter
	UV //= 1								-- Set top-left UV integer
	local x:number	= UV.x % tW				-- Wrap U to texture width
	local y:number	= ( UV.y % tH ) * tW	-- Wrap V to texture height
	local i:number	= (x + y) * u32B		-- Set index value
	return buffer_vec3(t.Buffer, i)			-- Return read value
end
local function Sampler_Linear( t: Textures.Texture, UV: vector ): vector
	local tW: number, tH: number = t.Width, t.Height
	UV *=				vector.create(tW, tH)		-- Expand resolution
	UV -=				vector.create( .5, .5 )		-- Shifts texture origin from top-left to middle of pixel
	local UV_0:vector =	UV // 1						-- Set top-left UV integer
	local UV_1:vector =	UV_0 + vec_1				-- Set bottom-right UV integer
	local A:vector =	UV - UV_0					-- Fetch XY lerp alpha
	local S0:number =	UV_0.x % tW					-- Fetch X0 index
	local S2:number =	UV_1.x % tW					-- Fetch X1 index
	local S1:number =	( UV_0.y % tH ) * tW		-- Fetch Y0 index
	local S3:number =	( UV_1.y % tH ) * tW		-- Fetch Y1 index
	local b:buffer =	t.Buffer					-- Fetch texture buffer
	S0, S1, S2, S3 = S0+S1, S2+S1, S0+S3, S2+S3		-- Calculate sample indexes
	S0 *= u32B; S1 *= u32B; S2 *= u32B; S3 *= u32B	-- Multiply sample indexes
	return vector.lerp(
		vector.lerp( buffer_vec3( b, S0 ), buffer_vec3( b, S1 ), A.x ),	-- Lerp X0Y0 -> X1Y0
		vector.lerp( buffer_vec3( b, S2 ), buffer_vec3( b, S3 ), A.x ),	-- Lerp X0Y1 -> X1Y1
		A.y																-- Lerp rows Y0 and Y1
	)
end

-- Math aliases
local radians, degrees, sin, cos, tan, asin, acos, atan, sinh, cosh, tanh, pow, exp, log, exp2, log2, sqrt, inversesqrt, abs, sign, floor, ceil, fract, mod, modf, min, max, clamp, mix, step, smoothstep, length, distance, dot, cross, normalize = -- Requiring implementation: asinh, acosh, atanh, exp2, inversesqrt, trunc, step, smoothstep, distance, faceforward, reflect, refract, determinant, outerProduct, matrixCompMult, inverse, transpose
	math.rad, math.deg, math.sin, math.cos, math.tan, math.asin, math.acos, math.atan, math.sinh, math.cosh, math.tanh, math.pow, math.exp, math.log, math.exp, function( n:f64 ):f64 return math.log( n, 2. ) end, math.sqrt, math.sqrt, math.abs, math.sign, math.floor, math.ceil, function( n:f64 ):f64 return n - math.floor(n) end, function( n:f64, m:f64 ):f64 return n % m end, math.modf, math.min, math.max, math.clamp, math.lerp, function( edge:f64, x:f64 ):f64 return x end, function(e0:f64,e1:f64,x:f64):f64 local t:f64=math.clamp((x-e0)/(e1-e0),0.,1.) return t*t*(3-2*t) end, vector.magnitude, vector.magnitude, vector.dot, vector.cross, vector.normalize