--!strict
local BufferStream = {};
BufferStream.__index = BufferStream;

function BufferStream.new(size: number)
    return setmetatable({
        len = 0,
        buffer = buffer.create(size),
        position = 0,
        capacity = size,
    }, BufferStream);
end

function BufferStream.fromBuffer(buf: buffer)
    local len = buffer.len(buf);
    return setmetatable({
        len = len,
        buffer = buf,
        position = 0,
        capacity = len,
    }, BufferStream);
end

export type BufferStream = typeof(BufferStream.new(0));

local function grow(current: number, minimum: number): number
    local new = math.max(current, 4);
    while true do
        new += (new/2) // 1;
        if (new >= minimum) then
            return new;
        end
    end
end

function BufferStream.move(self: BufferStream, delta: number)
    self.position += delta;
    if (self.position < 0) then
        error("BufferStream position is negative");
    end
end

function BufferStream.reset(self: BufferStream)
    self.position = 0;
end

function BufferStream.couldRead(self: BufferStream, size: number)
    return self.position + size <= self.capacity;
end

function BufferStream.expandToSizePrecise(self: BufferStream, new_size: number)
    if (new_size <= self.capacity) then
        return;
    end

    local current_size = self.capacity;
    self.capacity = new_size;

    local current = self.buffer;
    self.buffer = buffer.create(new_size);
    buffer.copy(self.buffer, 0, current, 0, current_size);
end

function BufferStream.expandToSize(self: BufferStream, new_size: number)
    self.len = new_size;
    if (new_size <= self.capacity) then
        return;
    end
    local predicted_size = grow(self.capacity, new_size);
    self:expandToSizePrecise(predicted_size)
end

function BufferStream.trim(self: BufferStream)
    local final = buffer.create(self.position);
    buffer.copy(final, 0, self.buffer, 0, self.len);

    self.capacity = self.len;
    self.buffer = final;
end

function BufferStream.readi8(self: BufferStream): number
    local value = buffer.readi8(self.buffer, self.position);
    self.position += 1;
    return value;
end

function BufferStream.readu8(self: BufferStream): number
    local value = buffer.readu8(self.buffer, self.position);
    self.position += 1;
    return value;
end

function BufferStream.readi16(self: BufferStream): number
    local value = buffer.readi16(self.buffer, self.position);
    self.position += 2;
    return value;
end

function BufferStream.readu16(self: BufferStream): number
    local value = buffer.readu16(self.buffer, self.position);
    self.position += 2;
    return value;
end

function BufferStream.readi32(self: BufferStream): number
    local value = buffer.readi32(self.buffer, self.position);
    self.position += 4;
    return value;
end

function BufferStream.readu32(self: BufferStream): number
    local value = buffer.readu32(self.buffer, self.position);
    self.position += 4;
    return value;
end

function BufferStream.readf32(self: BufferStream): number
    local value = buffer.readf32(self.buffer, self.position);
    self.position += 4;
    return value;
end

function BufferStream.readf64(self: BufferStream): number
    local value = buffer.readf64(self.buffer, self.position);
    self.position += 8;
    return value;
end

function BufferStream.readstring(self: BufferStream): string
    local len = buffer.readu32(self.buffer, self.position);
    self.position += 4;
    local value = buffer.readstring(self.buffer, self.position, len);
    self.position += len;
    return value;
end

function BufferStream.readbuffer(self: BufferStream, len: number): buffer
    local copy = buffer.create(len);
    buffer.copy(copy, 0, self.buffer, self.position, len);
    self.position += len;
    return copy;
end

function BufferStream.writei8(self: BufferStream, value: number)
    self:expandToSize(self.position + 1);
    buffer.writei8(self.buffer, self.position, value);
    self.position += 1;
end

function BufferStream.writeu8(self: BufferStream, value: number)
    self:expandToSize(self.position + 1);
    buffer.writeu8(self.buffer, self.position, value);
    self.position += 1;
end

function BufferStream.writei16(self: BufferStream, value: number)
    self:expandToSize(self.position + 2);
    buffer.writei16(self.buffer, self.position, value);
    self.position += 2;
end

function BufferStream.writeu16(self: BufferStream, value: number)
    self:expandToSize(self.position + 2);
    buffer.writeu16(self.buffer, self.position, value);
    self.position += 2;
end

function BufferStream.writei32(self: BufferStream, value: number)
    self:expandToSize(self.position + 4);
    buffer.writei32(self.buffer, self.position, value);
    self.position += 4;
end

function BufferStream.writeu32(self: BufferStream, value: number)
    self:expandToSize(self.position + 4);
    buffer.writeu32(self.buffer, self.position, value);
    self.position += 4;
end

function BufferStream.writef32(self: BufferStream, value: number)
    self:expandToSize(self.position + 4);
    buffer.writef32(self.buffer, self.position, value);
    self.position += 4;
end

function BufferStream.writef64(self: BufferStream, value: number)
    self:expandToSize(self.position + 8);
    buffer.writef64(self.buffer, self.position, value);
    self.position += 8;
end

function BufferStream.writerawstring(self: BufferStream, value: string, count: number?)
    count = count or #value;
    assert(type(count) == "number", "Invalid count");
    self:expandToSize(self.position + count);
    buffer.writestring(self.buffer, self.position, value, count);
    self.position += count;
end

function BufferStream.writestring(self: BufferStream, value: string, count: number?)
    count = count or #value;
    assert(type(count) == "number", "Invalid count");
    self:expandToSize(self.position + count + 4);
    buffer.writeu32(self.buffer, self.position, count);
    self.position += 4;
    buffer.writestring(self.buffer, self.position, value, count);
    self.position += count;
end

function BufferStream.writebuffer(self: BufferStream, value: buffer, count: number?)
    count = count or buffer.len(value);
    assert(type(count) == "number", "Invalid count");
    self:expandToSize(self.position + count);
    buffer.copy(self.buffer, self.position, value, 0, count);
    self.position += count;
end

return BufferStream;


--!strict
local roblox = require("@self/roblox");
local binary = require("@self/binary");
local xml = require("@self/xml");

export type DOM = roblox.Document;
export type DOMInfo = roblox.DocumentInfo;
export type DOMClass = roblox.Class;
export type DOMInstance = roblox.Instance;
export type DOMProperty = roblox.Property;

local BINARY_HEADER_LEN = #binary.header;

local dom = {};

dom.roblox = roblox;

function dom.deserialize(data: string | buffer): roblox.Document
    if (type(data) == "string") then
        data = buffer.fromstring(data);
    end
    assert(type(data) == "buffer", "Internal DOM Error");

    if (
        buffer.len(data) > BINARY_HEADER_LEN
        and buffer.readstring(data, 0, BINARY_HEADER_LEN) == binary.header
    ) then
        -- Binary
        return binary.deserialize(data, BINARY_HEADER_LEN);
    else
        -- XML
        return xml.deserialize(data);
    end
end

function dom.serialize(document: roblox.Document, format: ("XML" | "BINARY")?): buffer
    format = format or "BINARY";
    if (format == "BINARY") then
        return binary.serialize(document);
    else
        return xml.serialize(document);
    end
end

function dom.setDebugCallback(format: "XML" | "BINARY", callback: (...any) -> ()): ()
    format = format or "BINARY";
    if (format == "BINARY") then
        binary.debug(callback);
    else
        xml.debug(callback);
    end
end

return dom;


--!strict
--!native
--!optimize 2
local COLORS = {
    black = 30,
    red = 31,
    green = 32,
    yellow = 33,
    blue = 34,
    magenta = 35,
    cyan = 36,
    white = 37,
}

local STYLES = {
    reset = 0,
    bold = 1,
    dim = 2,
    italic = 3,
    underline = 4,
    blink = 5,
    reverse = 7,
    hidden = 8,
}

for name, code in COLORS do
    COLORS[name] = `\x1b[{code}m`;
end

for name, code in STYLES do
    STYLES[name] = `\x1b[{code}m`;
end

return {
    color = function(name: string)
        assert(COLORS[name], "Unknown color");
        return COLORS[name];
    end,
    style = function(kind: string)
        assert(STYLES[kind], "Unknown style");
        return STYLES[kind];
    end,
    reset = function(kind: string?)
        return `\x1b[0m`;
    end,
};


--!native
--!optimize 2

-- stylua: ignore
local HEX_TO_BINARY = {
    ["0"] = "0000", ["1"] = "0001", ["2"] = "0010", ["3"] = "0011",
    ["4"] = "0100", ["5"] = "0101", ["6"] = "0110", ["7"] = "0111",
    ["8"] = "1000", ["9"] = "1001", ["a"] = "1010", ["b"] = "1011",
    ["c"] = "1100", ["d"] = "1101", ["e"] = "1110", ["f"] = "1111",
}

--- A constant representing the 64-bit representation of `0`.
local ZERO = vector.create(0, 0, 0)
--- A constant representating the 64-bit representation of `1`.
local ONE = vector.create(0, 0, 1)
--- A constant representating the 64-bit representation of `2`.
local TWO = vector.create(0, 0, 2)

--- A constant representing the maximum possible 64-bit value
--- (`18446744073709551615`, or `2 ^ 64 - 1`)
local MAX = vector.create(0x3fffff, 0xfffff, 0x3fffff)
--- A constant representing the maximum possible 32-bit value
--- (`4294967295`, or `2 ^ 32 - 1`).
local MAX_U32 = vector.create(0, 0x3ff, 0x3fffff)
--- A constant representing the maximum integer that is exactly representable
--- by a normal Luau number (`9007199254740992` or `2 ^ 53`).
local MAX_F64 = vector.create(0x800, 0, 0)
--- A constant representing the maximum integer that is exactly representable
--- by a 32-bit floating point value (`16777216`, or `2 ^ 24`).
local MAX_F32 = vector.create(0, 4, 0)

--[=[
	Normalizes a vector, trimming off any bits that would interfere with
	the library's functionality.
]=]
local function normalize(u64: vector): vector
	return vector.create(
		bit32.band(u64.x, 0b1111_1111_1111_1111_1111_11),
		bit32.band(u64.y, 0b1111_1111_1111_1111_1111),
		bit32.band(u64.z, 0b1111_1111_1111_1111_1111_11)
	)
end

--[=[
	Constructs a 64-bit integer from the passed `u32` values.

	The provided `most` value fills the upper 32 bits of the integer,
	and the provided `least` value fills the lower 32-bits.
]=]
local function from_pair(most: number, least: number): vector
	-- 32 - 22 = 10
	-- x = upper 22 of most
	-- y = lower 10 of most and upper 10 of least
	-- z = lower 22 of least
	return vector.create(
		bit32.rshift(most, 10),
		bit32.bor(bit32.extract(least, 22, 10), bit32.lshift(bit32.band(most, 0b1111_1111_11), 10)),
		bit32.band(least, 0b1111_1111_1111_1111_1111_11)
	)
end

--[=[
	Constructs a 64-bit integer from the passed `u32`.

	This number is truncated in a 32-bit integer, even if it is larger than
	the max value.
]=]
local function from_u32(u32: number): vector
	return vector.create(0, bit32.rshift(u32, 22), bit32.band(u32, 0b1111_1111_1111_1111_1111_11))
end

--[=[
	Constructs a 64-bit integer from the passed `f64`.
	Put more plainly, converts a normal Luau number to a 64-bit integer.

	The number is truncated into an integer first. Note that Luau numbers
	cannot accurately represent integers past `2 ^ 53`.
]=]
local function from_f64(f64: number): vector
	return from_pair(bit32.bor(f64 // 2 ^ 32), bit32.bor(f64 % 2 ^ 32))
end

--[=[
	Converts the provided `u64` into two 32-bit integers.

	This returns the most significant portion of the number first.
]=]
local function to_pair(u64: vector): (number, number)
	-- x = upper 22 of most
	-- y = lower 10 of most and upper 10 of least
	-- z = lower 22 of least
	local most = bit32.bor(bit32.lshift(u64.x, 10), bit32.rshift(u64.y, 10))
	local least = bit32.bor(bit32.lshift(u64.y, 22), u64.z)

	return most, least
end

--[=[
	Converts the provided `u64` into four 16-bit integers.

	This returns the most significant portion of the number first.
]=]
local function to_quartet(u64: vector): (number, number, number, number)
	-- 0000000000000000000000 00000000000000000000 0000000000000000000000
	-- 0000000000000000 000000 0000000000 0000000000 000000 0000000000000000
	-- 1111000111100010 1101001111000100 1000000010011010 1011110011011110
	local most, least = to_pair(u64)
	local q1, q2 = bit32.rshift(most, 16), bit32.band(most, 0xffff)
	local q3, q4 = bit32.rshift(least, 16), bit32.band(least, 0xffff)
	-- local q1 = bit32.rshift(u64.x, 6)
	-- local q2 = bit32.bor(bit32.lshift(u64.y, 16), bit32.rshift(u64.y, 10))
	-- local q3 = bit32.bor(bit32.band(u64.y, 0b1111_1111_11), bit32.rshift(u64.z, 16))
	-- local q4 = bit32.band(0b1111_1111_1111_1111)
	return q1, q2, q3, q4
end

--[=[
	Converts the provided `u64` to an `f64` (or put plainly: a normal Luau
	number).

	This function does not check for precision loss. `f64` values lose precision
	past `2 ^ 53`.
]=]
local function to_f64(u64: vector): number
	local most, least = to_pair(u64)
	return most * 2 ^ 32 + least
end

--[=[
	Returns whether the provided `u64` is zero or not.

	Equivalent to `u64 == from_u32(0)`.
]=]
local function is_zero(u64: vector): boolean
	return u64 == ZERO
end

--[=[
	Computes the bitwise AND of the two provide values.

	This does not accept a vararg like the `bit32` equivalent for performance
	reasons.
]=]
local function band(lhs: vector, rhs: vector): vector
	return vector.create(bit32.band(lhs.x, rhs.x), bit32.band(lhs.y, rhs.y), bit32.band(lhs.z, rhs.z))
end

--[=[
	Computes the bitwise OR of the two provide values.

	This does not accept a vararg like the `bit32` equivalent for performance
	reasons.
]=]
local function bor(lhs: vector, rhs: vector): vector
	return vector.create(bit32.bor(lhs.x, rhs.x), bit32.bor(lhs.y, rhs.y), bit32.bor(lhs.z, rhs.z))
end

--[=[
	Computes the bitwise XOR of the two provide values.

	This does not accept a vararg like the `bit32` equivalent for performance
	reasons.
]=]
local function bxor(lhs: vector, rhs: vector): vector
	return vector.create(bit32.bxor(lhs.x, rhs.x), bit32.bxor(lhs.y, rhs.y), bit32.bxor(lhs.z, rhs.z))
end

--[=[
	Computes the bitwise negation of the provided value.
]=]
local function bnot(u64: vector): vector
	return vector.create(
		bit32.band(bit32.bnot(u64.x), 0b1111_1111_1111_1111_1111_11),
		bit32.band(bit32.bnot(u64.y), 0b1111_1111_1111_1111_1111),
		bit32.band(bit32.bnot(u64.z), 0b1111_1111_1111_1111_1111_11)
	)
end

--[=[
	Shifts the provided `u64` logically left by `n` bits.

	This function will error if `n` is negative.
]=]
local function lshift(u64: vector, n: number): vector
	if n >= 64 then
		return vector.zero
	elseif n == 0 then
		return u64
	end
	local most, least = to_pair(u64)
	if n < 32 then
		local remainder = 32 - n
		local ret = from_pair(bit32.replace(bit32.rshift(least, remainder), most, n, remainder), bit32.lshift(least, n))
		return ret
	else
		local ret = from_pair(bit32.lshift(least, n - 32), 0)
		return ret
	end
end

--[=[
	Shifts the provided `u64` logically right by `n` bits.

	This function will error if `n` is negative.
]=]
local function rshift(u64: vector, n: number): vector
	if n == 0 then
		return u64
	end
	local most, least = to_pair(u64)
	if n < 32 then
		local ret = from_pair(bit32.rshift(most, n), bit32.replace(bit32.rshift(least, n), most, 32 - n, n))
		return ret
	else
		local ret = from_pair(0, bit32.rshift(most, n - 32))
		return ret
	end
end

--[=[
	Shifts the provided `u64` arithmetically right by `n` bits. Since these
	numbers are unsigned, this effectively just copies the most significant
	bit into the empty space rather than filling them with zeros.

	This function will error if `n` is negative.
]=]
local function arshift(u64: vector, n: number): vector
	-- If the sign bit is set, we do arshift
	if bit32.btest(u64.x, 0x20_0000) then
		if n >= 64 then
			return MAX
		else
			return bor(rshift(u64, n), bnot(rshift(MAX, n)))
		end
	else
		-- Otherwise it's just an rshift
		return rshift(u64, n)
	end
end

--[=[
	Rotates the bits of the provided `u64` left by `n` bits.

	If `n` is negative, this is equivalent to `rrotate`. Otherwise, if `n` is
	greater than 64-bits, it will wrap around.
]=]
local function lrotate(u64: vector, n: number): vector
	n = bit32.band(n, 63)
	local lshifted = lshift(u64, n)
	local rshifted = rshift(u64, 64 - n)
	return bor(lshifted, rshifted)
end

--[=[
	Rotates the bits of the provided `u64` right by `n` bits.

	If `n` is negative, this is equivalent to `lrotate`. Otherwise, if `n` is
	greater than 64-bits, it will wrap around.
]=]
local function rrotate(u64: vector, n: number): vector
	n = bit32.band(n, 63)
	local lshifted = lshift(u64, 64 - n)
	local rshifted = rshift(u64, n)
	return bor(lshifted, rshifted)
end

--[=[
	Returns the number of consecutive zero bits in the provided `u64` starting
	from the left-most (most significant) bit.
]=]
local function countlz(u64: vector): number
	local most, least = to_pair(u64)
	if most == 0 then
		return bit32.countlz(least) + 32
	else
		return bit32.countlz(most)
	end
end

--[=[
	Returns the number of consecutive zero bits in the provided `u64` starting
	from the right-most (least significant) bit.
]=]
local function countrz(u64: vector): number
	local most, least = to_pair(u64)
	if least == 0 then
		return bit32.countrz(most) + 32
	else
		return bit32.countrz(least)
	end
end

--[=[
	Returns a boolean describing whether the bitwise AND of `lhs` and
	`rhs` are different than zero.

	This does not accept a vararg like the `bit32` equivalent for performance
	reasons.
]=]
local function btest(lhs: vector, rhs: vector): boolean
	return band(lhs, rhs) ~= ZERO
end

--[=[
	Returns the provided `u64` with the order of bytes swapped.
]=]
local function byteswap(u64: vector): vector
	local most, least = to_pair(u64)
	return from_pair(bit32.byteswap(least), bit32.byteswap(most))
end

--[=[
	Returns whether `lhs` is greater than `rhs`.
]=]
local function gt(lhs: vector, rhs: vector): boolean
	local l_most, l_least = to_pair(lhs)
	local r_most, r_least = to_pair(rhs)

	if l_most == r_most then
		return l_least > r_least
	else
		return l_most > r_most
	end
end

--[=[
	Returns whether `lhs` is greater than or equal to `rhs`.
]=]
local function gt_equal(lhs: vector, rhs: vector): boolean
	return lhs == rhs or gt(lhs, rhs)
end

--[=[
	Returns whether `lhs` is less than `rhs`.
]=]
local function lt(lhs: vector, rhs: vector): boolean
	local l_most, l_least = to_pair(lhs)
	local r_most, r_least = to_pair(rhs)

	if l_most == r_most then
		return l_least < r_least
	else
		return l_most < r_most
	end
end

--[=[
	Returns whether `lhs` is less than or equal to `rhs`.
]=]
local function lt_equal(lhs: vector, rhs: vector): boolean
	return lhs == rhs or lt(lhs, rhs)
end

--[=[
	Calculates the quotient of the two provided values and returns it,
	along with the remainder from that operation.

	This function operates on a single 64-bit integer and a 32-bit integer.
	For dividing two 64-bit integers, see `div`.

	Equivalent to `//` for normal integers.
	This function will error if `divisor` is 0.
]=]
local function div_32(dividend: vector, divisor: number): (vector, number)
	if divisor == 0 then
		error("cannot divide integers by zero")
	end
	local hi, lo = to_pair(dividend)
	local q_hi, q_lo = 0, 0
	local rem = 0

	for i = 31, 0, -1 do
		rem = bit32.bor(bit32.lshift(rem, 1), bit32.band(bit32.rshift(hi, i), 1))

		if rem >= divisor then
			rem -= divisor
			q_hi = bit32.bor(q_hi, bit32.lshift(1, i))
		end
	end
	for i = 31, 0, -1 do
		rem = bit32.bor(bit32.lshift(rem, 1), bit32.band(bit32.rshift(lo, i), 1))

		if rem >= divisor then
			rem -= divisor
			q_lo = bit32.bor(q_lo, bit32.lshift(1, i))
		end
	end

	return from_pair(q_hi, q_lo), rem
end

--[=[
	Calculates the sum of the two provided values. Equivalent to `+` for
	normal integers.

	If the sum is equal to or greater than `2 ^ 64`, the returned value will
	overflow rather than expanding beyond 64 bits.
]=]
local function add(augend: vector, addend: vector): vector
	local prime = augend + addend

	prime += vector.create(0, if prime.z > (2 ^ 22 - 1) then 1 else 0, 0)
	prime += vector.create(if prime.y > (2 ^ 20 - 1) then 1 else 0, 0, 0)

	return normalize(prime)
end

--[=[
	Calculates the difference of the two provided values. Equivalent to `-` for
	normal integers.

	If the difference is less than `0`, the returned value will overflow rather
	than going negative.
]=]
local function sub(minuend: vector, subtrahend: vector): vector
	local prime = minuend - subtrahend

	local adjust_z = prime.z
	local adjust_y = prime.y
	local adjust_x = prime.x

	local borrow_z = prime.z < 0
	if borrow_z then
		adjust_z += 2 ^ 22
	end

	if borrow_z then
		adjust_y -= 1
	end
	local borrow_y = adjust_y < 0
	if borrow_y then
		adjust_y += 2 ^ 20
	end

	if borrow_y then
		adjust_x -= 1
	end
	local borrow_x = adjust_x < 0
	if borrow_x then
		adjust_x += 2 ^ 22
	end

	return vector.create(adjust_x, adjust_y, adjust_z)
end

--[=[
	Calculates the product of the two provided values. Equivalent to `*` for
	normal integers.

	If the product is greater than or equal to `2 ^ 64`, the returned value will
	overflow rather than expanding beyond 64 bits.
]=]
local function mult(multiplier: vector, multiplicand: vector): vector
	-- We represent 64-bit numbers as two 32-bit ones.
	-- Multiplying them is:
	-- (A + B) * (C + D) = (A * C) + (A * D) + (B * C) + (B * D)
	--
	-- However, multiplying two 32-bit numbers might overflow. So, we need to
	-- use 16-bit numbers. This turns our math into this:
	-- (A + B + C + D) * (E + F + G + H) =
	-- 		(A * E) + (A * F) + (A * G) + (A * H) +
	-- 		(B * E) + (B * F) + (B * G) + (B * H) +
	-- 		(C * E) + (C * F) + (C * G) + (C * H) +
	-- 		(D * E) + (D * F) + (D * G) + (D * H)
	--
	-- We can skip (A * E), (A * F), (A * G), (B * E), (B * F), and (C * F)
	-- because they don't exist within the bounds of the final product.
	-- Since the numbers are built as A * 2 ^ 48 + B * 2 ^ 32 + C * 2 ^ 16 + D
	-- you end up with e.g. (A * 2 ^ 48 * E * 2 ^ 48) which is A * E * 2^96.
	-- Otherwise... Here we go.

	local a, b, c, d = to_quartet(multiplier)
	local e, f, g, h = to_quartet(multiplicand)

	local product_4 = d * h
	local product_3 = bit32.rshift(product_4, 16) + c * h
	local product_2 = bit32.rshift(product_3, 16)
	product_3 = bit32.band(product_3, 0xFFFF) + d * g

	product_2 += bit32.rshift(product_3, 16) + b * h
	local product_1 = bit32.rshift(product_2, 16)

	product_2 = bit32.band(product_2, 0xFFFF) + c * g
	product_1 += bit32.rshift(product_2, 16)
	product_2 = bit32.band(product_2, 0xFFFF) + d * f
	product_1 += bit32.rshift(product_2, 16) + a * h + b * g + c * f + d * e

	-- We skip truncating any of the products the last time since they'll never
	-- overflow and bit32 will truncate them for us.

	return from_pair(bit32.replace(product_2, product_1, 16, 16), bit32.replace(product_4, product_3, 16, 16))
end

--[=[
	Calculates the quotient of the two provided values and returns it, along
	with the remainder.

	Equivalent to `//` and `%` for normal integers.
	This function will error if `divisor` is `0`.
]=]
local function div(dividend: vector, divisor: vector): (vector, vector)
	if is_zero(divisor) then
		error("cannot divide integers by zero")
	elseif countlz(dividend) >= 11 and countlz(divisor) >= 11 then
		local l_real = to_f64(dividend)
		local r_real = to_f64(divisor)
		return from_f64(l_real // r_real), from_f64(l_real % r_real)
	end

	local quotient = ZERO
	local remainder = dividend

	-- Doing extra iterations of this is very bad!
	for i = math.abs(countlz(dividend) - countlz(divisor)), 0, -1 do
		local shifted_divisor = lshift(divisor, i)

		if gt_equal(remainder, shifted_divisor) then
			remainder = sub(remainder, shifted_divisor)
			quotient = bor(quotient, lshift(ONE, i))
		end
	end

	return quotient, remainder
end

--[=[
	Calculates the result of `base` raised to `power`. Equivalent to `^` for
	normal integers. `power` is interpreted as a 32-bit integer.

	If the result is greater than or equal to `2 ^ 64`, the returned value will
	overflow rather than expanding beyond 64 bits.

	Additionally, `0 ^ 0` is treated as being `1`.
]=]
local function pow(base: vector, power: number): vector
	-- TODO: Is there an efficient solution that isn't recursion?
	if power == 0 then
		return ONE
	elseif power % 2 == 1 then
		return mult(base, pow(base, power - 1))
	else
		local temp = pow(base, power / 2)
		return mult(temp, temp)
	end
end

--[=[
	Reads a `u64` from the provided buffer. If offset is provided, reads from
	that spot in the buffer. Otherwise, reads from the beginning.

	This function reads a little-endian value from the buffer.
]=]
local function from_buffer(buf: buffer, offset: number?): vector
	offset = offset or 0
	local least = buffer.readu32(buf, offset :: number)
	local most = buffer.readu32(buf, offset :: number + 4)

	return from_pair(most, least)
end

--[=[
	Reads a `u64` from the provided string. If offset is provided, reads from
	that point in the string. Otherwise starts at the beginning.

	This function reads a little-endian value from the string.
]=]
local function from_byte_string(str: string, offset: number?): vector
	offset = offset or 1
	-- This is deliberately backwards because we expect a little endian input
	local d, c, b, a, h, g, f, e = string.byte(str, offset, offset :: number + 7)
	local least = bit32.bor(bit32.lshift(bit32.replace(b, a, 8, 8), 16), bit32.replace(d, c, 8, 8))
	local most = bit32.bor(bit32.lshift(bit32.replace(f, e, 8, 8), 16), bit32.replace(h, g, 8, 8))

	return from_pair(most, least)
end

--[=[
	Converts the provided `u64` to a buffer.

	The integer is written as a little-endian value.
]=]
local function to_buffer(u64: vector): buffer
	local most, least = to_pair(u64)
	local buf = buffer.create(8)
	-- We use a little endian format because it doesn't matter and most
	-- systems we run on are little endian so it'll be nicer for them.
	buffer.writeu32(buf, 0, least)
	buffer.writeu32(buf, 4, most)
	return buf
end

--[=[
	Writes the provided `u64` to the provided buffer. The value is written
	to the buffer at the provided offset.

	The integer is written as a little-endian value.
]=]
local function write_to_buffer(b: buffer, u64: vector, offset: number)
	local most, least = to_pair(u64)
	buffer.writeu32(b, offset, least)
	buffer.writeu32(b, offset + 4, most)
end

--[=[
	Converts the provided `u64` to string of binary data.

	The integer is written as if it were little-endian.
]=]
local function to_byte_string(u64: vector): string
	local most, least = to_pair(u64)
	-- Previous implementation of this used `%c` but it turns out that
	-- `string.format("%c", 0)` results in a zero length string!
	return string.char(
		bit32.band(least, 0xFF),
		bit32.extract(least, 8, 8),
		bit32.extract(least, 16, 8),
		bit32.rshift(least, 24),
		bit32.band(most, 0xFF),
		bit32.extract(most, 8, 8),
		bit32.extract(most, 16, 8),
		bit32.rshift(most, 24)
	)
end

--[=[
	Converts the provided `u64` to a string of 16 hexadecimal digits.

	The returned string will always be 16 bytes and the number is formatted as
	if it were big-endian. It also always uses lowercase letters.
]=]
local function to_hex_string(u64: vector): string
	local most, least = to_pair(u64)
	return string.format("%08x%08x", most, least)
end

--[=[
	Converts the provided `u64` to a string of 64 binary digits.

	The returned string will always be 64 bytes and the number is formatted as
	if it were big-endian.
]=]
local function to_bin_string(u64: vector): string
	-- Manually implemtning a binary conversion is not any faster, and
	-- this is a lot easier.
	return (string.gsub(to_hex_string(u64), "(.)", HEX_TO_BINARY))
end

--[=[
	Returns the provided value parsed as a 64-bit integer. Expects the provided
	string to contain only the digits 0-9 and will error if it does not.

	Leading zeros are accepted and parsed appropriately.

	Due to the implementation, the resulting integer will wrap around if it is
	too large to fit in a 64-bit integer rather than erroring.
]=]
local function from_dec_string(str: string): vector
	if string.find(str, "%D") then
		local pos, digit = string.match(str, "()(%D)")
		local hex = string.format("0x%02x", string.byte(digit :: any))
		error(`unexpected character '{digit}' ({hex}) at position {pos} while parsing decimal string`)
	end
	local low, high = 0, 0
	for i = 1, #str do
		-- The ascii code for zero is `48`.
		local digit = string.byte(str, i, i) - 48

		low *= 10
		high = high * 10 + low // 2 ^ 32
		low = bit32.bor(low, 0) + digit

		if low >= 2 ^ 32 then
			low -= 2 ^ 32
			high += 1
		end
	end

	return from_pair(high, low)
end

--[=[
	Returns the provided value as a string of decimal digits.

	Equivalent to formatting normal numbers with `%u` or
	calling `tostring` on them.
]=]
local function to_dec_string(u64: vector): string
	if countlz(u64) >= 11 then
		return tostring(to_f64(u64))
	else
		local q, rem = div_32(u64, 1e9)
		local q_hi, q_lo = to_pair(q)
		if q_hi == 0 and q_lo < 1e9 then
			return string.format("%u%09u", q_lo, rem)
		else
			return string.format("%s%09u", to_dec_string(q), rem)
		end
	end
end

-- TODO: replace and extract
-- TODO: vararg versions of band, bor, bxor

return {
	--- A constant representing the 64-bit representation of `0`.
	ZERO = ZERO,
	--- A constant representating the 64-bit representation of `1`.
	ONE = ONE,
	--- A constant representating the 64-bit representation of `2`.
	TWO = TWO,
	--- A constant representing the maximum possible 64-bit value
	--- (`18446744073709551615`, or `2 ^ 64 - 1`)
	MAX = MAX,
	--- A constant representing the maximum possible 32-bit value
	--- (`4294967295`, or `2 ^ 32 - 1`).
	MAX_U32 = MAX_U32,
	--- A constant representing the maximum integer that is exactly representable
	--- by a normal Luau number (`9007199254740992` or `2 ^ 53`).
	MAX_F64 = MAX_F64,
	--- A constant representing the maximum integer that is exactly representable
	--- by a 32-bit floating point value (`16777216`, or `2 ^ 24`).
	MAX_F32 = MAX_F32,

	from_pair = from_pair,
	from_u32 = from_u32,
	from_f64 = from_f64,

	to_pair = to_pair,
	to_quartet = to_quartet,
	to_f64 = to_f64,

	lt = lt,
	lt_equal = lt_equal,
	gt = gt,
	gt_equal = gt_equal,
	is_zero = is_zero,

	band = band,
	bor = bor,
	bxor = bxor,
	btest = btest,

	bnot = bnot,
	byteswap = byteswap,
	countlz = countlz,
	countrz = countrz,

	rshift = rshift,
	arshift = arshift,
	rrotate = rrotate,
	lshift = lshift,
	lrotate = lrotate,

	add = add,
	sub = sub,
	mult = mult,
	div = div,
	pow = pow,

	to_byte_string = to_byte_string,
	from_byte_string = from_byte_string,

	to_buffer = to_buffer,
	write_to_buffer = write_to_buffer,
	from_buffer = from_buffer,

	to_hex_string = to_hex_string,
	to_bin_string = to_bin_string,

	to_dec_string = to_dec_string,
	from_dec_string = from_dec_string,
}


--!strict
export type Class = {
    index: number,
    name: string,
    isService: boolean,
    instances: {number},
};

export type Property = {
    type: number,
    value: any,
}

export type Instance = {
    className: string,
    referent: number,
    parent: number?,
    properties: {[string]: Property},
};

export type Signature = {
    Type: "Ed25519",
    PublicKey: buffer,
    Value: string,
};

export type SharedString = string;

export type DocumentInfo = {
    version: number,
    classes: number,
    instances: number,
    reserved: buffer,
};

export type Document = {
    info: DocumentInfo,
    instances: {Instance},
    classes: {Class},
    metadata: {[string]: string},
    sharedstrings: {[string]: SharedString},
    signatures: {Signature}?,
    --- internal use only.
    --- would ideally be nil
    __sharedstrings_id: {string}?,
};

local PROPERTY_TYPES = {
    Unknown = 0,
    String = 1,
    Bool = 2,
    Int = 3,
    Float = 4,
    Double = 5,
    UDim = 6,
    UDim2 = 7,
    Ray = 8,
    Faces = 9,
    Axes = 10,
    BrickColor = 11,
    Color3 = 12,
    Vector2 = 13,
    Vector3 = 14,
    -- Padded
    CFrame = 16,
    Quaternion = 17,
    Enum = 18,
    Ref = 19,
    Vector3int16 = 20,
    NumberSequence = 21,
    ColorSequence = 22,
    NumberRange = 23,
    Rect = 24,
    PhysicalProperties = 25,
    Color3uint8 = 26,
    Int64 = 27,
    SharedString = 28,
    OptionalCFrame = 30,
    UniqueId = 31,
    Font = 32,
    SecurityCapabilities = 33,
    Content = 34,
}

return {
    PropertyTypes = PROPERTY_TYPES,
}


--!strict
local polyfill = require("@polyfill");
local mem = polyfill.mem;

local xml = {};

export type Node = {
    tag: string,
    attributes: { [string]: string },
    children: string | { Node },
    cdata: boolean?,
    raw: boolean?,
}

local function parseAttributes(attrStr: string): { [string]: string }
    local attrs = {};
    for key, value in string.gmatch(attrStr, '([%w_]+)="([^"]*)"') do
        attrs[key] = assert(value, `missing attribute value`);
    end
    return attrs;
end

local function escapeText(text: string): string
    text = string.gsub(text, "&", "&");
    text = string.gsub(text, "<", "<");
    text = string.gsub(text, ">", ">");
    text = string.gsub(text, "\"", """);
    text = string.gsub(text, "'", "'");
    return text;
end

local function unescapeText(text: string): string
    text = string.gsub(text, "<", "<");
    text = string.gsub(text, ">", ">");
    text = string.gsub(text, """, "\"");
    text = string.gsub(text, "'", "'");
    text = string.gsub(text, "&", "&");
    return text;
end

local function CDATA(text: string): string
    if (string.find(text, "[<=/\\>&]") ~= nil) then
        return `<![CDATA[{text}]]>`;
    else
        return text;
    end
end

local function parse(buf: buffer): Node?
    local len = buffer.len(buf);
    local pos = 0
    local parent_stack: {Node} = {}
    local root: Node? = nil
    while (pos < len) do
        if (buffer.readu8(buf, pos) == 60) then -- '<'
            if (mem.eqlSlice(buf, pos + 1, "![CDATA[", 0, 8)) then -- CDATA
                local end_pos = mem.findPos(buf, pos + 9, "]]>");
                if (end_pos) then
                    local text = buffer.readstring(buf, pos + 9, end_pos - pos - 9);
                    if (#parent_stack > 0) then
                        local parent = parent_stack[#parent_stack];
                        parent.cdata = true;
                        parent.children = text;
                    end
                    pos = end_pos + 3;
                else
                    error("unclosed CDATA")
                end
            elseif (mem.eqlSlice(buf, pos + 1, "!--", 0, 3)) then -- Comment
                local end_pos = mem.findPos(buf, pos + 4, "-->");
                if end_pos then
                    pos = end_pos + 3;
                else
                    error("unclosed comment");
                end
            else -- Tag
                local tag_end = assert(mem.findAnyPos(buf, pos + 1, "\32>"), "invalid tag");
                local tag_name = buffer.readstring(buf, pos + 1, tag_end - pos - 1);
                local is_closing = string.sub(tag_name, 1, 1) == '/';
                if (is_closing) then
                    tag_name = string.sub(tag_name, 2);
                    assert(#parent_stack > 0 and parent_stack[#parent_stack].tag == tag_name, `mismatched closing tag: {tag_name}`);
                    local node = assert(table.remove(parent_stack), "unreachable");
                    local close_pos = assert(mem.findScalarPos(buf, tag_end, string.byte('>')), "invalid closing tag");
                    pos = close_pos + 1;
                    if (type(node.children) == "table" and #node.children == 0) then
                        node.children = "";
                    end
                else -- Opening tag
                    local attr_end = assert(mem.findScalarPos(buf, tag_end, string.byte('>')), "invalid tag");
                    local attr_str = buffer.readstring(buf, tag_end + 1, attr_end - tag_end);
                    local indep_closing = string.sub(attr_str, -1) == '/';
                    if (indep_closing) then
                        attr_str = string.sub(attr_str, 1, -2);
                    end
                    local attrs = parseAttributes(attr_str);
                    local node: Node = {
                        tag = tag_name,
                        attributes = attrs,
                        children = {},
                    }
                    if (#parent_stack > 0) then
                        local parent = parent_stack[#parent_stack];
                        table.insert(parent.children :: any, node);
                    else
                        root = node;
                    end
                    if (not indep_closing) then
                        table.insert(parent_stack, node);
                    end
                    pos = attr_end + 1;
                end
            end
        else -- Text content
            local text_end = mem.findScalarPos(buf, pos, string.byte('<')) or len + 1;
            local content = buffer.readstring(buf, pos, text_end - pos);
            local text = string.gsub((string.gsub(content, "^%s+", '')), "%s+$", '');
            if (text ~= '' and #parent_stack > 0) then
                local parent = parent_stack[#parent_stack];
                parent.children = unescapeText(text);
            end
            pos = text_end;
        end
    end

    if (#parent_stack > 0) then
        assert(false, `unclosed tag: {parent_stack[#parent_stack].tag}`);
    end

    return root;
end

function xml.parse(buf: buffer)
    return parse(buf);
end

local function serializeAttributes(attrs: { [string]: string }): string
    local parts = {}
    for key, value in pairs(attrs) do
        table.insert(parts, string.format(`%s="%s"`, key, value))
    end
    return table.concat(parts, ' ')
end

local function serializeNode(node: Node, depth: number): string
    local parts = {}
    local attr_str = serializeAttributes(node.attributes)
    local tag_str = string.format('<%s%s>', node.tag, if (attr_str ~= "") then ` {attr_str}` else "")
    local indent = string.rep('\t', depth)
    table.insert(parts, indent)
    table.insert(parts, tag_str)
    local children_len = #node.children
    if (type(node.children) == "string") then
        if (node.cdata) then
            table.insert(parts, CDATA(node.children))
        else
            if (node.raw) then
                table.insert(parts, node.children)
            else
                table.insert(parts, escapeText(node.children))
            end
        end
    else
        for i, child in node.children do
            table.insert(parts, '\n')
            table.insert(parts, serializeNode(child, depth + 1))
        end
    end
    if (children_len > 0 and type(node.children) ~= "string") then
        table.insert(parts, '\n')
        table.insert(parts, indent)
    end
    table.insert(parts, string.format('</%s>', node.tag))
    return table.concat(parts, "");
end

function xml.stringify(node: Node): string
    return serializeNode(node, 0)
end

return xml


local dom = require("@self/dom");
local manager = require("@self/core/manager");
local reflection = require("@self/reflection");
local polyfill = require("@polyfill");

local module = {};

module.dom = dom;
module.manager = manager;
module.reflection = reflection;
module.polyfill = polyfill;

function module.deserializePlace(contents: string | buffer)
    local content = if (type(contents) == "string") then buffer.fromstring(contents) else contents;
    local docs = dom.deserialize(content);
    return manager.placeFromDom(docs);
end

function module.deserializeModel(contents: string | buffer)
    local content = if (type(contents) == "string") then buffer.fromstring(contents) else contents;
    local docs = dom.deserialize(content);
    return manager.modelFromDOM(docs);
end

function module.serializePlace(dataModel: any, xml: boolean?): buffer
    local docs = manager.domFromPlace(dataModel);
    return dom.serialize(docs, if (xml) then "XML" else "BINARY");
end

function module.serializeModel(instances: {any}, xml: boolean?): buffer
    local docs = manager.domFromModel(instances);
    return dom.serialize(docs, if (xml) then "XML" else "BINARY");
end

function module.getReflectionDatabase(): reflection.ReflectionDatabase
    return reflection;
end

return module;


--!strict
--!native
--!optimize 2

type TimeDictionary = {
    Year: number,
    Month: number,
    Day: number,
    Hour: number,
    Minute: number,
    Second: number,
    Millisecond: number,
}

local start_time = os.time()
local start_clock = os.clock()

local DateTime = {
    UnixTimestamp = (nil :: any) :: number,
    UnixTimestampMillis = (nil :: any) :: number,
}

DateTime.__type = "DateTime"
DateTime.__index = function(self: RobloxDateTime, key: string): any
    if (key == "UnixTimestamp") then
        return self.UnixTimestampMillis // 1000;
    end
    return DateTime[key];
end
DateTime.__newindex = function(self: RobloxDateTime, key: string, value: any): ()
    error(`DateTime is read-only`);
end
DateTime.__tostring = function(self: RobloxDateTime): string
    return tostring(self.UnixTimestampMillis);
end

DateTime.__eq = function(left: RobloxDateTime, right: RobloxDateTime): boolean
    return left.UnixTimestampMillis == right.UnixTimestampMillis;
end

function DateTime.toUniversalTime(self: RobloxDateTime): TimeDictionary
    error("Not implemented");
end

function DateTime.ToLocalTime(self: RobloxDateTime): TimeDictionary
    error("Not implemented");
end

function DateTime.FormatLocalTime(self: RobloxDateTime, formatString: string?, locale: string?): string
    error("Not implemented");
end

function DateTime.FormatUniversalTime(self: RobloxDateTime, formatString: string?, locale: string?): string
    error("Not implemented");
end

function DateTime.ToIsoDate(self: RobloxDateTime): string
    error("Not implemented");
end

type RobloxDateTime = typeof(DateTime);

local function newDateTime(timestampMillis: number): RobloxDateTime
    return setmetatable({
        UnixTimestampMillis = timestampMillis,
    }, DateTime) :: any
end

return {
    now = function(): RobloxDateTime
        return newDateTime(((start_time + (os.clock() - start_clock)) * 1000)// 1)
    end,
    fromUnixTimestamp = function(unixTimestamp: number): RobloxDateTime
        return newDateTime((unixTimestamp // 1) * 1000);
    end,
    fromUnixTimestampMillis = function(unixTimestampMillis: number): RobloxDateTime
        return newDateTime(unixTimestampMillis // 1);
    end,
    fromUniversalTime = function(
        year: number?,
        month: number?,
        day: number?,
        hour: number?,
        minute: number?,
        second: number?,
        millisecond: number?
    ): RobloxDateTime
        error("Not implemented");
    end,
    fromLocalTime = function(
        year: number?,
        month: number?,
        day: number?,
        hour: number?,
        minute: number?,
        second: number?,
        millisecond: number?
    ): RobloxDateTime
        error("Not implemented");
    end,
    fromIsoDate = function(isoDate: string): RobloxDateTime
        error("Not implemented");
    end,
};


--!strict
return {
    base64 = require("@self/base64"),
    blake2 = require("@self/blake2"),
    datetime = require("@self/datetime"),
    lz4 = require("@self/lz4"),
    md5 = require("@self/md5"),
    mem = require("@self/mem"),
    sink = require("@self/sink"),
    stream = require("@self/stream"),
    task = require("@self/task"),
    zlib = require("@self/zlib"),
    zstd = require("@self/zstd"),
}


--!strict
--!native
--!optimize 2

type IoWritable = {
    write: (self: IoWritable, value: string | buffer) -> (),
    writei8: (self: IoWritable, value: number) -> (),
    writeu8: (self: IoWritable, value: number) -> (),
    writei16: (self: IoWritable, value: number) -> (),
    writeu16: (self: IoWritable, value: number) -> (),
    writei32: (self: IoWritable, value: number) -> (),
    writeu32: (self: IoWritable, value: number) -> (),
    writef32: (self: IoWritable, value: number) -> (),
    writef64: (self: IoWritable, value: number) -> (),
}

local BufferSink = {};
BufferSink.__index = BufferSink;

function BufferSink.create(size: number?)
    return setmetatable({
        len = 0,
        buffer = buffer.create(size or 0),
        position = 0,
        capacity = size or 0,
    }, BufferSink);
end

export type BufferSink = typeof(BufferSink.create(0));

local function grow(current: number, minimum: number): number
    local new = math.max(current, 4);
    while true do
        new += (new/2) // 1;
        if (new >= minimum) then
            return new;
        end
    end
end

function BufferSink.writer(self: BufferSink): IoWritable
    return self :: any;
end

function BufferSink.expandToSizePrecise(self: BufferSink, new_size: number)
    if (new_size <= self.capacity) then
        return;
    end

    local current_size = self.capacity;
    self.capacity = new_size;

    local current = self.buffer;
    self.buffer = buffer.create(new_size);
    buffer.copy(self.buffer, 0, current, 0, current_size);
end

function BufferSink.expandToSize(self: BufferSink, new_size: number)
    self.len = new_size;
    if (new_size <= self.capacity) then
        return;
    end
    local predicted_size = grow(self.capacity, new_size);
    self:expandToSizePrecise(predicted_size)
end

function BufferSink.flush(self: BufferSink, bytes: boolean?) : string | buffer
    local final = buffer.create(self.position);
    buffer.copy(final, 0, self.buffer, 0, self.position);
    self.capacity = self.len;
    if (bytes == false) then
        return buffer.tostring(final);
    end
    return final;
end

function BufferSink.write(self: BufferSink, value: string | buffer)
    if (type(value) == "string") then
        local amt = #value;
        self:expandToSize(self.position + amt);
        buffer.writestring(self.buffer, self.position, value)
        self.position += amt;
        return;
    else
        local amt = buffer.len(value);
        self:expandToSize(self.position + amt);
        buffer.copy(self.buffer, self.position, value);
        self.position += amt;
        return;
    end
end

function BufferSink.writei8(self: BufferSink, value: number)
    self:expandToSize(self.position + 1);
    buffer.writei8(self.buffer, self.position, value);
    self.position += 1;
end

function BufferSink.writeu8(self: BufferSink, value: number)
    self:expandToSize(self.position + 1);
    buffer.writeu8(self.buffer, self.position, value);
    self.position += 1;
end

function BufferSink.writei16(self: BufferSink, value: number)
    self:expandToSize(self.position + 2);
    buffer.writei16(self.buffer, self.position, value);
    self.position += 2;
end

function BufferSink.writeu16(self: BufferSink, value: number)
    self:expandToSize(self.position + 2);
    buffer.writeu16(self.buffer, self.position, value);
    self.position += 2;
end

function BufferSink.writei32(self: BufferSink, value: number)
    self:expandToSize(self.position + 4);
    buffer.writei32(self.buffer, self.position, value);
    self.position += 4;
end

function BufferSink.writeu32(self: BufferSink, value: number)
    self:expandToSize(self.position + 4);
    buffer.writeu32(self.buffer, self.position, value);
    self.position += 4;
end

function BufferSink.writef32(self: BufferSink, value: number)
    self:expandToSize(self.position + 4);
    buffer.writef32(self.buffer, self.position, value);
    self.position += 4;
end

function BufferSink.writef64(self: BufferSink, value: number)
    self:expandToSize(self.position + 8);
    buffer.writef64(self.buffer, self.position, value);
    self.position += 8;
end

function BufferSink.writestring(self: BufferSink, value: string, count: number?)
    count = count or #value;
    assert(type(count) == "number", "Invalid count");
    self:expandToSize(self.position + count + 4);
    buffer.writeu32(self.buffer, self.position, count);
    self.position += 4;
    buffer.writestring(self.buffer, self.position, value, count);
    self.position += count;
end

return BufferSink;


--!strict
--!native
--!optimize 2

export type IoWritable = {
    write: (self: IoWritable, value: string | buffer) -> (),
    writei8: (self: IoWritable, value: number) -> (),
    writeu8: (self: IoWritable, value: number) -> (),
    writei16: (self: IoWritable, value: number) -> (),
    writeu16: (self: IoWritable, value: number) -> (),
    writei32: (self: IoWritable, value: number) -> (),
    writeu32: (self: IoWritable, value: number) -> (),
    writef32: (self: IoWritable, value: number) -> (),
    writef64: (self: IoWritable, value: number) -> (),
}

export type IoReadable = {
    read:
        & ((self: IoReadable, amount: number?, bytes: true?) -> buffer)
        & ((self: IoReadable, amount: number?, bytes: false) -> string),
    readi8: (self: IoReadable) -> number,
    readu8: (self: IoReadable) -> number,
    readi16: (self: IoReadable) -> number,
    readu16: (self: IoReadable) -> number,
    readi32: (self: IoReadable) -> number,
    readu32: (self: IoReadable) -> number,
    readf32: (self: IoReadable) -> number,
    readf64: (self: IoReadable) -> number,
}

local BufferStream = {};
BufferStream.__index = BufferStream;

function BufferStream.create(buf: buffer)
    local len = buffer.len(buf);
    return setmetatable({
        len = len,
        buffer = buf,
        position = 0,
    }, BufferStream);
end

export type BufferStream = typeof(BufferStream.create(buffer.create(0)));

function BufferStream.reader(self: BufferStream): IoReadable
    return self :: any;
end

function BufferStream.writer(self: BufferStream): IoWritable
    return self :: any;
end

function BufferStream.pos(self: BufferStream): number
    return self.position;
end

function BufferStream.size(self: BufferStream): number
    return self.len;
end

function BufferStream.seekTo(self: BufferStream, pos: number)
    self.position = pos;
end

function BufferStream.seekBy(self: BufferStream, pos: number)
    self.position = math.clamp((self.position + pos) // 1, 0, self.len);
end

function BufferStream.canRead(self: BufferStream, size: number)
    return self.position + size <= self.len;
end

function BufferStream.read(self: BufferStream, amount: number?, bytes: boolean?): (string | buffer)?
    local len = amount or self.len - self.position;
    if (amount and amount <= 0) then
        return if (bytes == false) then
            ""
        else
            buffer.create(0);
    end
    if (bytes == false) then
        local value = buffer.readstring(self.buffer, self.position, len);
        self.position += len;
        return value;
    else
        local copy = buffer.create(len);
        buffer.copy(copy, 0, self.buffer, self.position, len);
        self.position += len;
        return copy;
    end
end

function BufferStream.readi8(self: BufferStream): number
    local value = buffer.readi8(self.buffer, self.position);
    self.position += 1;
    return value;
end

function BufferStream.readu8(self: BufferStream): number
    local value = buffer.readu8(self.buffer, self.position);
    self.position += 1;
    return value;
end

function BufferStream.readi16(self: BufferStream): number
    local value = buffer.readi16(self.buffer, self.position);
    self.position += 2;
    return value;
end

function BufferStream.readu16(self: BufferStream): number
    local value = buffer.readu16(self.buffer, self.position);
    self.position += 2;
    return value;
end

function BufferStream.readi32(self: BufferStream): number
    local value = buffer.readi32(self.buffer, self.position);
    self.position += 4;
    return value;
end

function BufferStream.readu32(self: BufferStream): number
    local value = buffer.readu32(self.buffer, self.position);
    self.position += 4;
    return value;
end

function BufferStream.readf32(self: BufferStream): number
    local value = buffer.readf32(self.buffer, self.position);
    self.position += 4;
    return value;
end

function BufferStream.readf64(self: BufferStream): number
    local value = buffer.readf64(self.buffer, self.position);
    self.position += 8;
    return value;
end

function BufferStream.write(self: BufferStream, value: string | buffer)
    if (type(value) == "string") then
        buffer.writestring(self.buffer, self.position, value)
        self.position += #value;
        return;
    else
        buffer.copy(self.buffer, self.position, value);
        return;
    end
end

function BufferStream.writei8(self: BufferStream, value: number)
    buffer.writei8(self.buffer, self.position, value);
    self.position += 1;
end

function BufferStream.writeu8(self: BufferStream, value: number)
    buffer.writeu8(self.buffer, self.position, value);
    self.position += 1;
end

function BufferStream.writei16(self: BufferStream, value: number)
    buffer.writei16(self.buffer, self.position, value);
    self.position += 2;
end

function BufferStream.writeu16(self: BufferStream, value: number)
    buffer.writeu16(self.buffer, self.position, value);
    self.position += 2;
end

function BufferStream.writei32(self: BufferStream, value: number)
    buffer.writei32(self.buffer, self.position, value);
    self.position += 4;
end

function BufferStream.writeu32(self: BufferStream, value: number)
    buffer.writeu32(self.buffer, self.position, value);
    self.position += 4;
end

function BufferStream.writef32(self: BufferStream, value: number)
    buffer.writef32(self.buffer, self.position, value);
    self.position += 4;
end

function BufferStream.writef64(self: BufferStream, value: number)
    buffer.writef64(self.buffer, self.position, value);
    self.position += 8;
end

return BufferStream;


--!strict
--!native
--!optimize 2
return {
    spawn = function(f: (() -> ()) | thread): thread
        if (type(f) == "function") then
            local thread = coroutine.create(f);
            assert(coroutine.resume(thread));
            return thread
        end
        assert(coroutine.resume(f));
        return f;
    end,
    wait = function(f: (() -> ()) | thread): (() -> ())
        error("Not implemented");
    end,
    delay = function(time: number, f: (() -> ()) | thread): thread
        error("Not implemented");
    end,
    defer = function(f: (() -> ()) | thread): thread
        error("Not implemented");
    end,
};


--!strict
return {
    compress = function(...)
        error("Not implemented")
    end::any,
    decompress = function(...)
        error("Not implemented")
    end::any,
};


--!strict
return {
    compress = function(...)
        error("Not implemented")
    end::any,
    decompress = function(...)
        error("Not implemented")
    end::any,
}


--!strict
--!native
--!optimize 2

type TimeDictionary = {
    Year: number,
    Month: number,
    Day: number,
    Hour: number,
    Minute: number,
    Second: number,
    Millisecond: number,
}

local lune_datetime = require("@lune/datetime") :: any;

local DateTime = {
    UnixTimestamp = (nil :: any) :: number,
    UnixTimestampMillis = (nil :: any) :: number,
    __internal = (nil :: any) :: any,
}

DateTime.__type = "DateTime"
DateTime.__index = function(self: RobloxDateTime, key: string): any
    if (key == "UnixTimestamp") then
        return self.__internal.unixTimestamp;
    elseif (key == "UnixTimestampMillis") then
        return self.__internal.unixTimestampMillis;
    end
    return DateTime[key];
end
DateTime.__newindex = function(self: RobloxDateTime, key: string, value: any): ()
    error(`DateTime is read-only`);
end
DateTime.__tostring = function(self: RobloxDateTime): string
    return tostring(self.__internal.unixTimestampMillis);
end

DateTime.__eq = function(left: RobloxDateTime, right: RobloxDateTime): boolean
    return left.__internal.unixTimestampMillis == right.__internal.unixTimestampMillis;
end

function DateTime.toUniversalTime(self: RobloxDateTime): TimeDictionary
    local time = self.__internal:toUniversalTime()
    return {
        Year = time.year,
        Month = time.month,
        Day = time.day,
        Hour = time.hour,
        Minute = time.minute,
        Second = time.second,
        Millisecond = time.millisecond,
    }
end

function DateTime.ToLocalTime(self: RobloxDateTime): TimeDictionary
    local time = self.__internal:toLocalTime()
    return {
        Year = time.year,
        Month = time.month,
        Day = time.day,
        Hour = time.hour,
        Minute = time.minute,
        Second = time.second,
        Millisecond = time.millisecond,
    }
end

function DateTime.FormatLocalTime(self: RobloxDateTime, formatString: string?, locale: string?): string
    return self.__internal:formatLocalTime(formatString or "%Y-%m-%d %H:%M:%S");
end

function DateTime.FormatUniversalTime(self: RobloxDateTime, formatString: string?, locale: string?): string
    return self.__internal:formatUniversalTime(formatString or "%Y-%m-%d %H:%M:%S");
end

function DateTime.ToIsoDate(self: RobloxDateTime): string
    return self.__internal:toIsoDate()
end

type RobloxDateTime = typeof(DateTime);

local function newDateTime(internal: any): RobloxDateTime
    return setmetatable({
        __internal = internal,
    }, DateTime) :: any
end

local datetime = {};

function datetime.now(): RobloxDateTime
    return newDateTime(lune_datetime.now());
end

function datetime.fromUnixTimestamp(unixTimestamp: number): RobloxDateTime
	return newDateTime(lune_datetime.fromUnixTimestamp(unixTimestamp));
end

function datetime.fromUnixTimestampMillis(unixTimestampMillis: number): RobloxDateTime
   return newDateTime(lune_datetime.fromUnixTimestampMillis(unixTimestampMillis));
end

function datetime.fromUniversalTime(
    year: number?,
    month: number?,
    day: number?,
    hour: number?,
    minute: number?,
    second: number?,
    millisecond: number?
): RobloxDateTime
    return newDateTime(lune_datetime.fromUniversalTime({
        year = year or 1970,
        month = month or 1,
        day = day or 1,
        hour = hour or 0,
        minute = minute or 0,
        second = second or 0,
        millisecond = millisecond or 0
    }));
end

function datetime.fromLocalTime(
    year: number?,
    month: number?,
    day: number?,
    hour: number?,
    minute: number?,
    second: number?,
    millisecond: number?
): RobloxDateTime
    return newDateTime(lune_datetime.fromLocalTime({
        year = year or 1970,
        month = month or 1,
        day = day or 1,
        hour = hour or 0,
        minute = minute or 0,
        second = second or 0,
        millisecond = millisecond or 0
    }));
end

function datetime.fromIsoDate(isoDate: string): RobloxDateTime
    return newDateTime(lune_datetime.fromIsoDate(isoDate));
end

return datetime;


--!strict
--!native
--!optimize 2
local serde = require("@lune/serde") :: any;

return {
    base64 = require("./luau/base64"), -- fallback
    blake2 = require("./luau/blake2"), -- fallback
    datetime = require("@self/datetime"),
    lz4 = require("./luau/lz4"), -- fallback
    md5 = function(value: string): string
        return serde.hash("md5", value);
    end,
    mem = require("./luau/mem"),
    sink = require("./luau/sink"),
    stream = require("./luau/stream"),
    task = require("@lune/task") :: any,
    zlib = {
        compress = function(src: buffer | string): buffer | string
            local isBuffer = type(src) == "buffer";
            local compressed = serde.compress("zlib", if (isBuffer) then buffer.tostring(src::buffer) else src::string);
            if (isBuffer) then
                return buffer.fromstring(compressed)
            end
            return compressed
        end::any,
        decompress = function(src: buffer | string, _: number): buffer | string
            local isBuffer = type(src) == "buffer";
            local decompressed = serde.decompress("zlib", if (isBuffer) then buffer.tostring(src::buffer) else src::string);
            if (isBuffer) then
                return buffer.fromstring(decompressed)
            end
            return decompressed
        end::any,
    },
    zstd = {
        compress = function(src: buffer | string): buffer | string
            local isBuffer = type(src) == "buffer";
            local compressed = serde.compress("zstd", if (isBuffer) then buffer.tostring(src::buffer) else src::string);
            if (isBuffer) then
                return buffer.fromstring(compressed)
            end
            return compressed
        end::any,
        decompress = function(src: buffer | string, _: number): buffer | string
            local isBuffer = type(src) == "buffer";
            local decompressed = serde.decompress("zstd", if (isBuffer) then buffer.tostring(src::buffer) else src::string);
            if (isBuffer) then
                return buffer.fromstring(decompressed)
            end
            return decompressed
        end::any
    },
}


--!strict
--!native
--!optimize 2
local task = require("@lute/task") :: any;
local crypto = require("@lute/crypto") :: any;

return {
    base64 = require("./luau/base64"), -- fallback
    blake2 = require("./luau/blake2"), -- fallback
    datetime = require("./luau/datetime"), -- fallback
    lz4 = require("./luau/lz4"), -- fallback
    md5 = function(value: string): string
        return buffer.tostring(crypto.digest(crypto.hash.md5, value));
    end,
    mem = require("./luau/mem"),
    sink = require("./luau/sink"),
    stream = require("./luau/stream"),
    task =  {
        spawn = task.spawn,
        wait = task.wait,
        delay = function(time: number, f: (() -> ()) | thread): thread
            return task.spawn(function()
                task.wait(time);
                if (type(f) == "function") then
                    f();
                else
                    task.resume(f);
                end
            end);
        end,
        defer = function(f: (() -> ()) | thread): thread
            return task.spawn(function()
                task.defer();
                if (type(f) == "function") then
                    f();
                else
                    task.resume(f);
                end
            end);
        end,
    },
    zlib = require("./luau/zlib"), -- fallback
    zstd = require("./luau/zstd"), -- fallback
}


--!strict
--!native
--!optimize 2

type TimeDictionary = {
    Year: number,
    Month: number,
    Day: number,
    Hour: number,
    Minute: number,
    Second: number,
    Millisecond: number,
}

local DateTime = {
    UnixTimestamp = (nil :: any) :: number,
    UnixTimestampMillis = (nil :: any) :: number,
    __internal = (nil :: any) :: any,
}

DateTime.__type = "DateTime"
DateTime.__index = function(self: RobloxDateTime, key: string): any
    if (key == "UnixTimestamp") then
        return self.__internal.timestamp;
    elseif (key == "UnixTimestampMillis") then
        return self.__internal.timestamp_millis;
    end
    return DateTime[key];
end
DateTime.__newindex = function(self: RobloxDateTime, key: string, value: any): ()
    error(`DateTime is read-only`);
end
DateTime.__tostring = function(self: RobloxDateTime): string
    return tostring(self.__internal.timestamp_millis);
end

DateTime.__eq = function(left: RobloxDateTime, right: RobloxDateTime): boolean
    return left.__internal.timestamp_millis == right.__internal.timestamp_millis;
end

function DateTime.toUniversalTime(self: RobloxDateTime): TimeDictionary
    local time = self.__internal:toUniversalTime()
    return {
        Year = time.year,
        Month = time.month,
        Day = time.day,
        Hour = time.hour,
        Minute = time.minute,
        Second = time.second,
        Millisecond = time.millisecond,
    }
end

function DateTime.ToLocalTime(self: RobloxDateTime): TimeDictionary
    local time = self.__internal:toLocalTime()
    return {
        Year = time.year,
        Month = time.month,
        Day = time.day,
        Hour = time.hour,
        Minute = time.minute,
        Second = time.second,
        Millisecond = time.millisecond,
    }
end

function DateTime.FormatLocalTime(self: RobloxDateTime, formatString: string?, locale: string?): string
    return self.__internal:formatLocalTime(formatString or "%Y-%m-%d %H:%M:%S");
end

function DateTime.FormatUniversalTime(self: RobloxDateTime, formatString: string?, locale: string?): string
    return self.__internal:formatUniversalTime(formatString or "%Y-%m-%d %H:%M:%S");
end

function DateTime.ToIsoDate(self: RobloxDateTime): string
    return self.__internal:toIsoDate()
end

type RobloxDateTime = typeof(DateTime);

local function newDateTime(internal: any): RobloxDateTime
    return setmetatable({
        __internal = internal,
    }, DateTime) :: any
end

local datetime = {};

function datetime.now(): RobloxDateTime
    return newDateTime(zune.time.date.now());
end

function datetime.fromUnixTimestamp(unixTimestamp: number): RobloxDateTime
	return newDateTime(zune.time.date.fromUnixTimestamp(unixTimestamp));
end

function datetime.fromUnixTimestampMillis(unixTimestampMillis: number): RobloxDateTime
   return newDateTime(zune.time.date.fromUnixTimestampMillis(unixTimestampMillis));
end

function datetime.fromUniversalTime(
    year: number?,
    month: number?,
    day: number?,
    hour: number?,
    minute: number?,
    second: number?,
    millisecond: number?
): RobloxDateTime
    return newDateTime(zune.time.date.fromUniversalTime(
        year or 1970,
        month or 1,
        day or 1,
        hour or 0,
        minute or 0,
        second or 0,
        millisecond or 0
    ));
end

function datetime.fromLocalTime(
    year: number?,
    month: number?,
    day: number?,
    hour: number?,
    minute: number?,
    second: number?,
    millisecond: number?
): RobloxDateTime
    return newDateTime(zune.time.date.fromLocalTime(
        year or 1970,
        month or 1,
        day or 1,
        hour or 0,
        minute or 0,
        second or 0,
        millisecond or 0
    ));
end

function datetime.fromIsoDate(isoDate: string): RobloxDateTime
    return newDateTime(zune.time.date.fromIsoDate(isoDate));
end

return datetime;


--!strict
--!native
--!optimize 2

return {
    base64 = zune.serde.base64,
    blake2 = {
        b128 = function(input, encoding: ("hex" | "base64")?): buffer | string
            local hash = zune.crypto.createHash("blake2b128");
            hash:update(input);
            return hash:digest(encoding or "binary");
        end
    },
    datetime = require("@self/datetime"),
    lz4 = zune.serde.lz4,
    md5 = function(input: string)
        local hash = zune.crypto.createHash("md5");
        hash:update(input);
        return hash:digest("hex");
    end,
    mem = zune.mem,
    sink = {
        create = function(size: number?): BufferSink
            return zune.io.createBufferSink({
                size = size,
            });
        end,
    },
    stream = {
        create = zune.io.createFixedBufferStream,
    },
    task = zune.task,
    zlib = zune.serde.zlib,
    zstd = zune.serde.zstd,
}


--!strict
--!native
--!optimize 2

export type IoWritable = {
    write: (self: IoWritable, value: string | buffer) -> (),
    writei8: (self: IoWritable, value: number) -> (),
    writeu8: (self: IoWritable, value: number) -> (),
    writei16: (self: IoWritable, value: number) -> (),
    writeu16: (self: IoWritable, value: number) -> (),
    writei32: (self: IoWritable, value: number) -> (),
    writeu32: (self: IoWritable, value: number) -> (),
    writef32: (self: IoWritable, value: number) -> (),
    writef64: (self: IoWritable, value: number) -> (),
}

export type IoReadable = {
    read:
        & ((self: IoReadable, amount: number?, bytes: true?) -> buffer)
        & ((self: IoReadable, amount: number?, bytes: false) -> string),
    readi8: (self: IoReadable) -> number,
    readu8: (self: IoReadable) -> number,
    readi16: (self: IoReadable) -> number,
    readu16: (self: IoReadable) -> number,
    readi32: (self: IoReadable) -> number,
    readu32: (self: IoReadable) -> number,
    readf32: (self: IoReadable) -> number,
    readf64: (self: IoReadable) -> number,
}

export type BufferStream = typeof(require("@self/impl/luau").stream.create(buffer.create(0)));

type Impl = {
    base64: typeof(require("@self/impl/luau/base64")),
    blake2: typeof(require("@self/impl/luau/blake2")),
    datetime: typeof(require("@self/impl/luau/datetime")),
    lz4: typeof(require("@self/impl/luau/lz4")),
    md5: typeof(require("@self/impl/luau/md5")),
    mem: typeof(require("@self/impl/luau/mem")),
    sink: typeof(require("@self/impl/luau/sink")),
    stream: typeof(require("@self/impl/luau/stream")),
    task: typeof(require("@self/impl/luau/task")),
    zlib: typeof(require("@self/impl/luau/zlib")),
    zstd: typeof(require("@self/impl/luau/zstd")),
}

if (type(_VERSION) == "string") then
    if (string.sub(_VERSION, 1, 4) == "zune") then
        return require("@self/impl/zune") :: Impl;
    elseif (string.sub(_VERSION, 1, 4) == "Lune") then
        return require("@self/impl/lune") :: Impl;
    end
end

-- other/unknown
return require("@self/impl/luau") :: Impl;


-- Luau code generated by api-dump.luau
--!strict
export type ThreadSafety = "Unsafe" | "ReadSafe" | "Safe"

export type Security = string | {
    Read: string,
    Write: string,
}

export type Capabilities = {
    Read: {string},
}

export type TypeInfo = {
    Category: string,
    Name: string,
}

export type ValueType = {
    Name: string,
    Type: TypeInfo,
    Tags: {string}
}

export type FunctionMemberInfo = {
    MemberType: "Function",
    Name: string,
    Tags: {string}?,
    Serialization: {
        CanLoad: boolean,
        CanSave: boolean,
    },
    Parameters: {ValueType},
    ReturnType: TypeInfo | {TypeInfo},
    ThreadSafety: ThreadSafety,
    Security: Security,
}

export type EventMemberInfo = {
    MemberType: "Event",
    Name: string,
    Tags: {string}?,
    Parameters: {
        {
            Name: string,
            Type: {
                Category: string,
                Name: string,
            },
            Tags: {string}
        }
    },
    ThreadSafety: ThreadSafety,
    Security: Security,
}

export type PropertyMemberInfo = {
    Category: string,
    MemberType: "Property",
    Capabilities: Capabilities,
    Name: string,
    Tags: {string}?,
    Default: string?,
    Serialization: {
        CanLoad: boolean,
        CanSave: boolean,
    },
    ValueType: {
        Category: string,
        Name: string,
    },
    ThreadSafety: ThreadSafety,
    Security: Security,
}

export type MemberInfo = FunctionMemberInfo | EventMemberInfo | PropertyMemberInfo

export type ClassInfo = {
    Name: string,
    Superclass: string,
    Tags: {string}?,
    MemoryCategory: string,
    Members: {MemberInfo},
}

export type EnumInfo = {
    Name: string,
    Tags: {string}?,
    Items: {
        {
            Name: string,
            Value: number,
            Tags: {string}?
        }
    }
}

export type ApiDump = {
    Version: number,
    Classes: {ClassInfo},
    Enums: {EnumInfo}
}

return {
    Version = "0.707.0.7070728",
    ApiDump = require("@self/api-dump") :: any,
}


--!strict
local dump = require("@self/generated");

local api_dump: dump.ApiDump = dump.ApiDump;

local ReflectionDatabase = {};

export type ApiDump = dump.ApiDump;
export type EnumInfo = dump.EnumInfo;
export type Security = dump.Security;
export type TypeInfo = dump.TypeInfo;
export type ClassInfo = dump.ClassInfo;
export type ValueType = dump.ValueType;
export type MemberInfo = dump.MemberInfo;
export type Capabilities = dump.Capabilities;
export type ThreadSafety = dump.ThreadSafety;
export type EventMemberInfo = dump.EventMemberInfo;
export type FunctionMemberInfo = dump.FunctionMemberInfo;
export type PropertyMemberInfo = dump.PropertyMemberInfo;

export type ReflectionDatabase = typeof(ReflectionDatabase);

local API_CLASS_MAP = {};
local API_CLASS_MEMBER_MAP = {};
local API_ENUM_MAP = {};

for _, enum in api_dump.Enums do
    API_ENUM_MAP[enum.Name] = enum;
end
for _, class in api_dump.Classes do
    API_CLASS_MAP[class.Name] = class;
    local member_map = {};
    for _, member in class.Members do
        member_map[member.Name] = member;
    end
    API_CLASS_MEMBER_MAP[class.Name] = member_map;
end

ReflectionDatabase.Version = dump.Version;
ReflectionDatabase.API_CLASS_MAP = API_CLASS_MAP;
ReflectionDatabase.API_CLASS_MEMBER_MAP = API_CLASS_MEMBER_MAP;
ReflectionDatabase.API_ENUM_MAP = API_ENUM_MAP;

function ReflectionDatabase.GetApiDump(self: ReflectionDatabase)
    return api_dump;
end

function ReflectionDatabase.GetClassNames(self: ReflectionDatabase): {string}
    local names = table.create(#api_dump.Classes);
    for _, class in api_dump.Classes do
        table.insert(names, class.Name);
    end
    return names;
end

function ReflectionDatabase.GetEnumNames(self: ReflectionDatabase): {string}
    local names = table.create(#api_dump.Enums);
    for _, enum in api_dump.Enums do
        table.insert(names, enum.Name);
    end
    return names;
end

function ReflectionDatabase.GetClass(self: ReflectionDatabase, name: string): dump.ClassInfo?
    return API_CLASS_MAP[name];
end

function ReflectionDatabase.GetClassMember(self: ReflectionDatabase, name: string, memberName: string, super: boolean?): dump.MemberInfo?
    local member_map = API_CLASS_MEMBER_MAP[name];
    if (not member_map or not member_map[memberName]) then
        if (not super) then
            return;
        end
        local class = API_CLASS_MAP[name];
        if (not class) then
            return;
        end
        if (class.Superclass and class.Superclass ~= "<<<ROOT>>>") then
            return self:GetClassMember(class.Superclass, memberName, super);
        end
        return;
    end
    return member_map[memberName];
end

function ReflectionDatabase.GetEnum(self: ReflectionDatabase, name: string): dump.EnumInfo?
    return API_ENUM_MAP[name];
end

function ReflectionDatabase.FindClass(self: ReflectionDatabase, name: string): dump.ClassInfo?
    name = name:lower();
    for _, class in api_dump.Classes do
        if (class.Name:lower() == name) then
            return class;
        end
    end
    return;
end

function ReflectionDatabase.FindEnum(self: ReflectionDatabase, name: string): dump.EnumInfo?
    name = name:lower();
    for _, enum in api_dump.Enums do
        if (enum.Name:lower() == name) then
            return enum;
        end
    end
    return;
end

return ReflectionDatabase;


local QueryAuthTypes = require(script.Parent.Parent.Types)

type AuthCondition = QueryAuthTypes.AuthCondition

--[=[
	Checks if a player's account age meets the specified minimum and maximum days.
	@function AccountAge
	@within QueryAuth
	@param minimum number -- Minimum account age in days.
	@param maximum number? -- Maximum account age in days. If nil, no maximum is enforced.
	@return AuthCondition -- A lambda that evaluates to see if the player's account age meets the criteria.
]=]
return function(minimum: number, maximum: number?): AuthCondition
	return function(player: Player): boolean
		local age = player.AccountAge

		return age >= minimum and (maximum == nil or age <= maximum)
	end
end


local BadgeService = game:GetService("BadgeService")
local QueryAuthTypes = require(script.Parent.Parent.Types)

type AuthCondition = QueryAuthTypes.AuthCondition

--[=[
	Checks if the player owns the badge with the given badge ID.
	@function Badge
	@within QueryAuth
	@param badgeId number -- The badge ID to check against.
	@return AuthCondition -- A lambda that evaluates to see if the player owns the specified badge.
]=]
return function(badgeId: number): AuthCondition
	return function(player: Player): boolean
		return BadgeService:UserHasBadgeAsync(player.UserId, badgeId)
	end
end


local QueryAuthTypes = require(script.Parent.Parent.Types)

type AuthCondition = QueryAuthTypes.AuthCondition

--[[
	Checks to see if the player is friends with another player.
	@function FriendsWith
	@within QueryAuth
	@param otherPlayerId number -- The UserId of the other player to check friendship with.
	@return AuthCondition -- A lambda that returns true if the player is friends with the other player.
]]
return function(otherPlayerId: number): AuthCondition
	return function(player: Player): boolean
		return player:IsFriendsWithAsync(otherPlayerId)
	end
end


local MarketplaceService = game:GetService("MarketplaceService")
local QueryAuthTypes = require(script.Parent.Parent.Types)

type AuthCondition = QueryAuthTypes.AuthCondition

--[=[
	Checks if the player owns the gamepass with the given asset ID.
	@function Gamepass
	@within QueryAuth
	@param gamepass number -- The gamepass asset ID to check against.
	@return AuthCondition -- A lambda that evaluates to see if the player owns the specified gamepass.
]=]
return function(gamepassId: number): AuthCondition
	return function(player: Player): boolean
		return MarketplaceService:UserOwnsGamePassAsync(player.UserId, gamepassId)
	end
end


--[=[
	A global time-based rate limiter, only returning true if you call it less than `times` amount of times within the `timeFrame` amount of time.
	@function GlobalThrottle
	@within QueryAuth
	@param timeFrame number -- The amount of time (seconds by default) between resetting the amount of calls that can happen.
	@param times number? -- The maximum amount of calls allowed within the given `timeFrame` (defaults to 1).
	@param mark (()->number)? -- An optional function which will be used to track intervals inside the function. (defaults to `os.clock`)
	@return AuthCondition -- A lambda that returns true if passed time parameter is greater or equal to time passed since last truthy return.
]=]
return function(timeFrame: number, times: number?, mark: (() -> number)?)
	local getTimeFn: () -> number = if mark then mark else os.clock
	times = times or 1

	local callsLeft = times
	local lastRefill = getTimeFn()

	return function(player: Player)
		local now = getTimeFn()
		local elapsed = now - lastRefill

		if elapsed >= timeFrame then
			callsLeft = times
			lastRefill = now
		end

		if callsLeft == 0 then
			return false
		end

		callsLeft -= 1
		return true
	end
end


local GroupService = game:GetService("GroupService")
local QueryAuthTypes = require(script.Parent.Parent.Types)

type AuthCondition = QueryAuthTypes.AuthCondition

--[=[
	Checks if the player is in the specified group with an optional rank range.
	@function Group
	@within QueryAuth
	@param group_id number -- The Roblox group ID to check against.
	@param min_rank number? | string? -- The minimum rank the player must have in the group.
	@param max_rank number? | string? -- The maximum rank the player can have in the group.
	@return AuthCondition -- A lambda that evaluates to see if the player is in the specified group with the given rank range.
]=]
return function(group_id: number, min_rank: number? | string?, max_rank: number? | string?): AuthCondition
	min_rank = min_rank or 1
	max_rank = max_rank or math.huge

	return function(player: Player): boolean
		local ok, info = pcall(function()
			return GroupService:GetGroupInfoAsync(group_id)
		end)
		if not ok then return false end

		local function toRank(v)
			if typeof(v) == "number" then return v end
			for _, r in info.Roles do
				if r.Name:lower() == tostring(v):lower() then
					return r.Rank
				end
			end

			return nil
		end

		local min, max = toRank(min_rank), toRank(max_rank)
		if not min or not max then return false end

		local rank = player:GetRankInGroupAsync(group_id)
		return rank >= min and rank <= max
	end
end


--[=[
	Checks to see if the player has not met this condition before.
	@function Once
	@within QueryAuth
	@return AuthCondition -- A lambda that returns true if the player did not get checked by the condition before.
]=]
return function()
    local used = {}

    return function(player: Player)
        if used[player] then
            return false
        end

        used[player] = true

        return true
    end
end


local QueryAuthTypes = require(script.Parent.Parent.Types)
local PolicyService = game:GetService("PolicyService")

type AuthCondition = QueryAuthTypes.AuthCondition
type Policy = QueryAuthTypes.Policy

--[=[
	Checks if PolicyService allows the specified policy for the player.
	@function Policy
	@within QueryAuth
	@param policy Policy -- The policy to check.
	@return AuthCondition -- A lambda that returns the value PolicyService provides.
]=]
return function(policy: Policy): AuthCondition
	return function(player: Player): boolean
        local success, result = pcall(function()
            return PolicyService:GetPolicyInfoForPlayerAsync(player)
        end)

        if not success then return false end

        return result[policy]
    end
end


local QueryAuthTypes = require(script.Parent.Parent.Types)

type AuthCondition = QueryAuthTypes.AuthCondition

--[=[
	Checks to see if the player has premium.
	@function Premium
	@within QueryAuth
	@return AuthCondition -- A lambda that returns true if the player has premium.
]=]
return function(): AuthCondition
	return function(player: Player): boolean
        return player.MembershipType == Enum.MembershipType.Premium
    end
end


local Workspace = game:GetService("Workspace")
local QueryAuthTypes = require(script.Parent.Parent.Types)

type AuthCondition = QueryAuthTypes.AuthCondition

--[=[
	Checks to see if the server's time is greater or equal to the provided time.
	@function ServerTime
	@within QueryAuth
	@param time number -- The time to check against, in seconds.
	@return AuthCondition -- A lambda that returns true if the server's time is greater or equal to the provided time.
]=]
return function(time: number): AuthCondition
	return function(_player: Player): boolean

        return Workspace.DistributedGameTime >= time
    end
end


local RunService = game:GetService("RunService")
local QueryAuthTypes = require(script.Parent.Parent.Types)

type AuthCondition = QueryAuthTypes.AuthCondition

--[=[
	Checks to see if the game is running in studio.
	@function Studio
	@within QueryAuth
	@return AuthCondition -- A lambda that returns true if game is running in studio.
]=]
return function(): AuthCondition
	return function(_player: Player): boolean
        return RunService:IsStudio()
    end
end


local QueryAuthTypes = require(script.Parent.Parent.Types)

type AuthCondition = QueryAuthTypes.AuthCondition

--[=[
	Checks if a player's team name is in the provided list of team names.
	@function Team
	@within QueryAuth
	@param values {string}? -- List of team names to check against. If nil, any team will pass.
	@return AuthCondition -- A lambda that evaluates to see if the player's team name is in the list.
]=]
return function(values: string?): AuthCondition
	return function(player: Player): boolean
        return values == nil or (player.Team ~= nil and player.Team.Name == values)
    end
end


--[=[
	A player time-based rate limiter, only returning true if you call it less than `times` amount of times within the `timeFrame` amount of time for a given player.
	@function Throttle
	@within QueryAuth
	@param timeFrame number -- The amount of time (seconds by default) between resetting the amount of calls that can happen.
	@param times number? -- The maximum amount of calls allowed within the given `timeFrame` (defaults to 1).
	@param mark (()->number)? -- An optional function which will be used to track intervals inside the function, defaults to `os.clock`
	@return AuthCondition -- A lambda that returns true if passed time parameter is greater or equal to time passed since last truthy return.
]=]
return function(timeFrame: number, times: number?, mark: (() -> number)?)
	local getTimeFn: () -> number = if mark then mark else os.clock
	times = times or 1

	local playerInfos = {} :: { [Player]: { callsLeft: number, lastRefill: number } }

	return function(player: Player)
		local playerInfo = playerInfos[player] or { callsLeft = times, lastRefill = getTimeFn() }
		local callsLeft = playerInfo.callsLeft
		local lastRefill = playerInfo.lastRefill

		local now = getTimeFn()
		local elapsed = now - lastRefill

		if elapsed >= timeFrame then
			callsLeft = times
			lastRefill = now
		end

		if callsLeft == 0 then
			return false
		end

		callsLeft -= 1
		playerInfos[player] = { callsLeft = callsLeft, lastRefill = lastRefill }
		return true
	end
end


local QueryAuthTypes = require(script.Parent.Parent.Types)

type AuthCondition = QueryAuthTypes.AuthCondition

--[=[
	Checks if a player's UserId is in the provided list of UserIds.
	@function UserId
	@within QueryAuth
	@param values {number}? -- List of UserIds to check against. If nil, no UserId will pass.
	@return AuthCondition -- A lambda that evaluates to see if the player's UserId is in the list.
]=]
return function(values: number?): AuthCondition
	return function(player: Player): boolean
		return values ~= nil and player.UserId == values
	end
end


--[=[
	@class Types
]=]

--[=[
	@type AuthCondition (player: Player) -> boolean
	@within Types
]=]
export type AuthCondition = (player: Player) -> boolean

--[=[
	A string which represents policies returned by [PolicyService.GetPolicyInfoForPlayerAsync](https://create.roblox.com/docs/reference/engine/classes/PolicyService#GetPolicyInfoForPlayerAsync).

	@type Policy "AreAdsAllowed" | "ArePaidRandomItemsRestricted" | "AllowedExternalLinkReferences" | "IsContentSharingAllowed" | "IsEligibleToPurchaseCommerceProduct" | "IsEligibleToPurchaseSubscription" | "IsPaidItemTradingAllowed" | "IsPhotoToAvatarAllowed" | "IsSubjectToChinaPolicies"
	@within Types
]=]
export type Policy =
    "AreAdsAllowed" |
    "ArePaidRandomItemsRestricted" |
    "AllowedExternalLinkReferences" |
    "IsContentSharingAllowed" |
    "IsEligibleToPurchaseCommerceProduct" |
    "IsEligibleToPurchaseSubscription" |
    "IsPaidItemTradingAllowed" |
    "IsPhotoToAvatarAllowed" |
    "IsSubjectToChinaPolicies"

return {}


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local QueryAuth = require(ReplicatedStorage.QueryAuth)

local plr = game.Players.LocalPlayer

local test = QueryAuth.any({
	QueryAuth.UserId(plr.UserId),
})

if test(plr) == true then
	print("🟢 Expected test1 to return true, got true")
else
	print("🔴 Expected test1 to return true, got false")
end

local test2 = QueryAuth.Policy("IsSubjectToChinaPolicies")
if test2(plr) == false then
	print("🟢 Expected test2 to return false, got false")
else
	print("🔴 Expected test2 to return false, got true")
end

local test3 = QueryAuth.Studio()
if test3(plr) == RunService:IsStudio() then
	print("🟢 Expected test3 to return true, got true")
else
	print("🔴 Expected test3 to return true, got false")
end

local test6 = QueryAuth.GlobalThrottle(1, 3)
do
	local passing = false

	local calls = {}

	for i = 1, 3 do
		calls[i] = test6(plr)
		task.wait(1)
	end

	passing = calls[1] == true and calls[2] == false and calls[3] == false

	if passing == true then
		print("🟢 Expected test6 to return true, got true")
	else
		print("🔴 Expected test6 to return false, got true")
	end
end

local test7 = QueryAuth.GlobalThrottle(1, 2)
do
	local passing = false

	local calls = {}

	for i = 1, 3 do
		calls[i] = test7(plr)
		task.wait(1)
	end

	passing = calls[1] == true and calls[2] == false and calls[3] == false

	if passing == true then
		print("🔴 Expected test7 to return false, got false")
	else
		print("🟢 Expected test7 to return false, got true")
	end
end


--!nonstrict

local proxyValue = require(script:WaitForChild("_Proxy"))

return proxyValue.value


--!strict

local ProxyTemplate = script:WaitForChild("ProxyTemplate") :: ModuleScript

type ProxyValue<T> = { value: T }

local function unknownRequire(instance: Instance)
	assert(instance:IsA("ModuleScript"), "The provided instance was not a ModuleScript.")
	return require(instance) :: any
end

local function createModuleProxy<T>(value: T)
	local proxyModule = ProxyTemplate:Clone()
	local proxy = unknownRequire(proxyModule:WaitForChild("_Proxy")) :: ProxyValue<T>
	proxy.value = value
	return proxyModule
end

local function addDescendantsProxies(parentModule, newParent)
	for _, child in parentModule:GetChildren() do
		if child:IsA("ModuleScript") then
			local value = unknownRequire(child)
			local proxyModule = createModuleProxy(value)
			proxyModule.Name = child.Name
			proxyModule.Parent = newParent

			addDescendantsProxies(child, proxyModule)
		elseif child:IsA("ValueBase") then
			local copyValue = child:Clone()
			copyValue.Parent = newParent

			copyValue.Changed:Connect(function(value)
				if child.Value ~= value then
					child.Value = value
				end
			end)

			child.Changed:Connect(function(value)
				copyValue.Value = value
			end)

			addDescendantsProxies(child, copyValue)
		end
	end
end

local function patch(CameraModule: ModuleScript)
	-- stylua: ignore
	local TransparencyController = unknownRequire(CameraModule:WaitForChild("TransparencyController")) :: { new: (...any) -> ...any }
	local oldTransparencyControllerNew = TransparencyController.new

	local result = nil
	local thread = coroutine.running()

	TransparencyController.new = function(...)
		-- set this back its original value so it behaves as expected next time it's called
		TransparencyController.new = oldTransparencyControllerNew

		-- get the parent function and call it
		-- this is equivalent to calling `result = CameraModule.new()`
		local cameraModuleNew = debug.info(2, "f")
		result = cameraModuleNew()

		-- unblock our waiting thread
		if coroutine.status(thread) == "suspended" then
			task.spawn(thread)
		end

		-- local activeThread = coroutine.running()
		-- task.defer(task.cancel, activeThread)

		-- yield forever so we don't continue to leak memory
		coroutine.yield()
	end

	-- the patch has been setup, now we wait!
	task.spawn(unknownRequire, CameraModule)

	while not result do
		coroutine.yield()
	end

	-- create a bunch of proxies so we don't actually have to move anything
	-- this ensures we don't breaks anything reliant on the original parent child relationship
	local newCameraModule = createModuleProxy(result)
	newCameraModule.Name = CameraModule.Name
	newCameraModule.Parent = CameraModule.Parent

	addDescendantsProxies(CameraModule, newCameraModule)

	CameraModule.Name = "_" .. CameraModule.Name
end

return patch


local Summon = require("./lib/Summon")
local config = require("@lib/config")()
local fs = require("@std/fs")
local net = require("@lute/net")
local process = require("@lute/process")

local ROBLOX_TYPEDEFS_FILENAME = "roblox.d.luau"

do
	fs.writestringtofile(
		ROBLOX_TYPEDEFS_FILENAME,
		net.request("https://luau-lsp.pages.dev/globalTypes.PluginSecurity.d.luau", { method = "GET" }).body
	)

	Summon.new("rojo", "sourcemap", "--output", config.sourcemap.filename, config.sourcemap.project):assert()
	process.exit(Summon.codeFromMany(
		Summon.new(
			"luau-lsp",
			"analyze",
			`--sourcemap={config.sourcemap.filename}`,
			'--ignore="**/include/**"',
			'--ignore="**/roblox_packages/**"',
			-- FIXME: luau-lsp is analyzing other WTH projects for some reason??
			"--ignore=../../../../../..",
			"--base-luaurc=.luaurc",
			`--definitions=roblox.d.luau`,
			"--flag:LuauSolverV2=true",
			"src"
		),
		Summon.new("selene", "src"),
		Summon.new("stylua", "--check", "src")
	))
end


local cli = {}
cli.__index = cli

type ArgKind = "positional" | "flag" | "option"
type ArgOptions = {
	help: string?,
	aliases: { string }?,
	default: string?,
	required: boolean?,
}

type ArgData = {
	name: string,
	kind: ArgKind,
	options: ArgOptions,
}

type ParseResult = {
	values: { [string]: string },
	flags: { [string]: boolean },
	fwdArgs: { string },
}

type ParserData = {
	arguments: { [number]: ArgData },
	positional: { [number]: ArgData },
	parsed: ParseResult,
}

type ParserInterface = typeof(cli)
export type Parser = setmetatable<ParserData, ParserInterface>

function cli.parser(): Parser
	local self = {
		arguments = {},
		positional = {},
		parsed = { values = {}, flags = {}, fwdArgs = {} },
	}

	return setmetatable(self, cli)
end

function cli.add(self: Parser, name: string, kind: ArgKind, options: ArgOptions): ()
	local argument: ArgData = {
		name = name,
		kind = kind,
		options = options or { aliases = {}, required = false },
	}

	table.insert(self.arguments, argument)
	if kind == "positional" then
		table.insert(self.positional, argument)
	end
end

function cli.parse(self: Parser, args: { string }): ()
	local i = 0
	local pos_index = 1

	while i < #args do
		i += 1

		local arg = args[i]

		-- if the argument is exactly "--", we pass everything along
		if arg == "--" then
			table.move(args, i + 1, #args, 1, self.parsed.fwdArgs)
			break
		end

		-- if the argument starts with two dashes, we're parsing either a flag or an option
		if string.sub(arg :: string, 1, 2) == "--" then
			local name = string.sub(arg :: string, 3)
			local found = false

			for _, argument in self.arguments do
				local aliases = argument.options.aliases or {}

				if argument.name == name or table.find(aliases, name) then
					found = true

					if argument.kind == "option" then
						-- advance past the argument
						i += 1

						assert(i <= #args, "Missing value for argument: " .. argument.name)
						self.parsed.values[argument.name] = args[i]

						break
					end

					self.parsed.flags[argument.name] = true
					break
				end
			end

			assert(found, "Unknown argument: " .. name)
			continue
		end

		-- if the argument starts with a single dash, we're parsing a flag
		if string.sub(arg :: string, 1, 1) == "-" then
			local flags = string.sub(arg :: string, 2)

			for j = 1, #flags do
				local name = string.sub(flags, j, j)
				local found = false
				for _, argument in self.arguments do
					local aliases = argument.options.aliases or {}
					if argument.name == name or table.find(aliases, name) then
						found = true
						if argument.kind == "option" then
							i += 1
							assert(i <= #args, "Missing value for argument: " .. argument.name)
							self.parsed.values[argument.name] = args[i]
						else
							self.parsed.flags[argument.name] = true
						end
						break
					end
				end

				assert(found, "Unknown argument: " .. name)
			end

			continue
		end

		-- if we have positional arguments left, we can take this argument as one
		if pos_index <= #self.positional then
			self.parsed.values[self.positional[pos_index].name] = arg
			pos_index += 1
			continue
		end

		-- otherwise, the argument is forwarded on
		table.insert(self.parsed.fwdArgs, arg)
	end

	-- check that all required arguments are present, and set any default values as needed
	for _, argument in self.arguments do
		-- selene: allow(incorrect_standard_library_use)
		assert(argument)

		if argument.options.required and self.parsed.values[argument.name] == nil :: any then
			assert(self.parsed.values[argument.name], "Missing required argument: " .. argument.name)
		end

		if self.parsed.values[argument.name] == nil :: any and argument.options.default then
			self.parsed.values[argument.name] = argument.options.default
		end
	end
end

function cli.get(self: Parser, name: string): string?
	return self.parsed.values[name]
end

function cli.has(self: Parser, name: string): boolean
	return self.parsed.flags[name] ~= nil
end

function cli.forwarded(self: Parser): { string }?
	return self.parsed.fwdArgs
end

function cli.help(self: Parser): ()
	print("Usage:")
	for _, argument in self.arguments do
		local aliasStr = table.concat(argument.options.aliases or {}, ", ")
		local reqStr = if argument.options.required then "(required) " else ""
		print(string.format("  --%s (-%s) - %s%s", argument.name, aliasStr, reqStr, argument.options.help or ""))
	end
end

return table.freeze(cli)