local binary_reader_class = require("../utilities/binary_reader")
local luau_spec = require("../luau/spec")

local version = {}
version.__index = version

function version.new(binary_reader: binary_reader_class.Class)
    local self = setmetatable({}, version)

    self.bytecode_version = binary_reader:read_u8()
    if (self.bytecode_version < luau_spec.LuauBytecodeTag.LBC_VERSION_MIN or self.bytecode_version > luau_spec.LuauBytecodeTag.LBC_VERSION_MAX) then
        error(`Bytecode version mismatch (expected [{luau_spec.LuauBytecodeTag.LBC_VERSION_MIN}..{luau_spec.LuauBytecodeTag.LBC_VERSION_MAX}], got {self.bytecode_version})`)
    end

    if (self.bytecode_version >= 4) then
        self.types_version = binary_reader:read_u8()
        if (self.types_version < luau_spec.LuauBytecodeTag.LBC_TYPE_VERSION_MIN or self.types_version > luau_spec.LuauBytecodeTag.LBC_TYPE_VERSION_MAX) then
            error(`Bytecode type version mismatch (expected [{luau_spec.LuauBytecodeTag.LBC_TYPE_VERSION_MIN}..{luau_spec.LuauBytecodeTag.LBC_TYPE_VERSION_MAX}], got {self.types_version})`)
        end
    end

    return self
end

export type Class = typeof(version.new(nil :: never))

return version


export type UserdataType = {
    index: number,
    name_ref: number,
}

local binary_reader_class = require("utilities/binary_reader")
local version_class = require("classes/version")
local string_table_class = require("classes/string_table")
local func_class = require("classes/func")

local deserializer = {}
deserializer.__index = deserializer

function deserializer.new(bytecode: buffer)
    local self = setmetatable({}, deserializer)

    self.binary_reader = binary_reader_class.new(bytecode)

    self.version = version_class.new(self.binary_reader)
    self.string_table = string_table_class.new(self.binary_reader)

    if (self.version.types_version == 3) then
        self.userdata_types = self:read_userdata_types()
    end

    self.functions = self:read_functions()

    self.main_function_index = self.binary_reader:read_var_int() + 1

    if (self.binary_reader.offset ~= buffer.len(bytecode)) then
        print(`WARNING: Bytecode was not parsed properly ({self.binary_reader.offset}/{buffer.len(bytecode)})`)
    end

    return self
end

function deserializer:read_functions()
    local functions_size: number = self.binary_reader:read_var_int()

    local functions: { func_class.Class } = table.create(functions_size)
    for function_index: number = 1, functions_size do
        functions[function_index] = func_class.new(self.binary_reader, self.string_table, self.version)
    end

    return functions
end

function deserializer:read_userdata_types()
    local userdata_types: { UserdataType } = {}

    while (true) do
        local index: number = self.binary_reader:read_u8()
        if (index == 0) then break end

        local name_ref: number = self.binary_reader:read_var_int()
        table.insert(userdata_types, { index = index, name_ref = name_ref })
    end

    return userdata_types
end

return deserializer


local LuauOpcode = {
    -- NOP: noop
    LOP_NOP = { index = 0, name = "NOP", has_aux = false },

    -- BREAK: debugger break
    LOP_BREAK = { index = 1, name = "BREAK", has_aux = false },

    -- LOADNIL: sets register to nil
    -- A: target register
    LOP_LOADNIL = { index = 2, name = "LOADNIL", has_aux = false },

    -- LOADB: sets register to boolean and jumps to a given short offset (used to compile comparison results into a boolean)
    -- A: target register
    -- B: value (0/1)
    -- C: jump offset
    LOP_LOADB = { index = 3, name = "LOADB", has_aux = false },

    -- LOADN: sets register to a number literal
    -- A: target register
    -- D: value (-32768..32767)
    LOP_LOADN = { index = 4, name = "LOADN", has_aux = false },

    -- LOADK: sets register to an entry from the constant table from the proto (number/vector/string)
    -- A: target register
    -- D: constant table index (0..32767)
    LOP_LOADK = { index = 5, name = "LOADK", has_aux = false },

    -- MOVE: move (copy) value from one register to another
    -- A: target register
    -- B: source register
    LOP_MOVE = { index = 6, name = "MOVE", has_aux = false },

    -- GETGLOBAL: load value from global table using constant string as a key
    -- A: target register
    -- C: predicted slot index (based on hash)
    -- AUX: constant table index
    LOP_GETGLOBAL = { index = 7, name = "GETGLOBAL", has_aux = true },

    -- SETGLOBAL: set value in global table using constant string as a key
    -- A: source register
    -- C: predicted slot index (based on hash)
    -- AUX: constant table index
    LOP_SETGLOBAL = { index = 8, name = "SETGLOBAL", has_aux = true },

    -- GETUPVAL: load upvalue from the upvalue table for the current function
    -- A: target register
    -- B: upvalue index
    LOP_GETUPVAL = { index = 9, name = "GETUPVAL", has_aux = false },

    -- SETUPVAL: store value into the upvalue table for the current function
    -- A: target register
    -- B: upvalue index
    LOP_SETUPVAL = { index = 10, name = "SETUPVAL", has_aux = false },

    -- CLOSEUPVALS: close (migrate to heap) all upvalues that were captured for registers >= target
    -- A: target register
    LOP_CLOSEUPVALS = { index = 11, name = "CLOSEUPVALS", has_aux = false },

    -- GETIMPORT: load imported global table global from the constant table
    -- A: target register
    -- D: constant table index (0..32767); we assume that imports are loaded into the constant table
    -- AUX: 3 10-bit indices of constant strings that, combined, constitute an import path; length of the path is set by the top 2 bits (1,2,3)
    LOP_GETIMPORT = { index = 12, name = "GETIMPORT", has_aux = true },

    -- GETTABLE: load value from table into target register using key from register
    -- A: target register
    -- B: table register
    -- C: index register
    LOP_GETTABLE = { index = 13, name = "GETTABLE", has_aux = false },

    -- SETTABLE: store source register into table using key from register
    -- A: source register
    -- B: table register
    -- C: index register
    LOP_SETTABLE = { index = 14, name = "SETTABLE", has_aux = false },

    -- GETTABLEKS: load value from table into target register using constant string as a key
    -- A: target register
    -- B: table register
    -- C: predicted slot index (based on hash)
    -- AUX: constant table index
    LOP_GETTABLEKS = { index = 15, name = "GETTABLEKS", has_aux = true },

    -- SETTABLEKS: store source register into table using constant string as a key
    -- A: source register
    -- B: table register
    -- C: predicted slot index (based on hash)
    -- AUX: constant table index
    LOP_SETTABLEKS = { index = 16, name = "SETTABLEKS", has_aux = true },

    -- GETTABLEN: load value from table into target register using small integer index as a key
    -- A: target register
    -- B: table register
    -- C: index-1 (index is 1..256)
    LOP_GETTABLEN = { index = 17, name = "GETTABLEN", has_aux = false },

    -- SETTABLEN: store source register into table using small integer index as a key
    -- A: source register
    -- B: table register
    -- C: index-1 (index is 1..256)
    LOP_SETTABLEN = { index = 18, name = "SETTABLEN", has_aux = false },

    -- NEWCLOSURE: create closure from a child proto; followed by a CAPTURE instruction for each upvalue
    -- A: target register
    -- D: child proto index (0..32767)
    LOP_NEWCLOSURE = { index = 19, name = "NEWCLOSURE", has_aux = false },

    -- NAMECALL: prepare to call specified method by name by loading function from source register using constant index into target register and copying source register into target register + 1
    -- A: target register
    -- B: source register
    -- C: predicted slot index (based on hash)
    -- AUX: constant table index
    -- Note that this instruction must be followed directly by CALL; it prepares the arguments
    -- This instruction is roughly equivalent to GETTABLEKS + MOVE pair, but we need a special instruction to support custom __namecall metamethod
    LOP_NAMECALL = { index = 20, name = "NAMECALL", has_aux = true },

    -- CALL: call specified function
    -- A: register where the function object lives, followed by arguments; results are placed starting from the same register
    -- B: argument count + 1, or 0 to preserve all arguments up to top (MULTRET)
    -- C: result count + 1, or 0 to preserve all values and adjust top (MULTRET)
    LOP_CALL = { index = 21, name = "CALL", has_aux = false },

    -- RETURN: returns specified values from the function
    -- A: register where the returned values start
    -- B: number of returned values + 1, or 0 to return all values up to top (MULTRET)
    LOP_RETURN = { index = 22, name = "RETURN", has_aux = false },

    -- JUMP: jumps to target offset
    -- D: jump offset (-32768..32767; 0 means "next instruction" aka "don't jump")
    LOP_JUMP = { index = 23, name = "JUMP", has_aux = false },

    -- JUMPBACK: jumps to target offset; this is equivalent to JUMP but is used as a safepoint to be able to interrupt while/repeat loops
    -- D: jump offset (-32768..32767; 0 means "next instruction" aka "don't jump")
    LOP_JUMPBACK = { index = 24, name = "JUMPBACK", has_aux = false },

    -- JUMPIF: jumps to target offset if register is not nil/false
    -- A: source register
    -- D: jump offset (-32768..32767; 0 means "next instruction" aka "don't jump")
    LOP_JUMPIF = { index = 25, name = "JUMPIF", has_aux = false },

    -- JUMPIFNOT: jumps to target offset if register is nil/false
    -- A: source register
    -- D: jump offset (-32768..32767; 0 means "next instruction" aka "don't jump")
    LOP_JUMPIFNOT = { index = 26, name = "JUMPIFNOT", has_aux = false },

    -- JUMPIFEQ, JUMPIFLE, JUMPIFLT, JUMPIFNOTEQ, JUMPIFNOTLE, JUMPIFNOTLT: jumps to target offset if the comparison is true (or false, for NOT variants)
    -- A: source register 1
    -- D: jump offset (-32768..32767; 1 means "next instruction" aka "don't jump")
    -- AUX: source register 2
    LOP_JUMPIFEQ = { index = 27, name = "JUMPIFEQ", has_aux = true },
    LOP_JUMPIFLE = { index = 28, name = "JUMPIFLE", has_aux = true },
    LOP_JUMPIFLT = { index = 29, name = "JUMPIFLT", has_aux = true },
    LOP_JUMPIFNOTEQ = { index = 30, name = "JUMPIFNOTEQ", has_aux = true },
    LOP_JUMPIFNOTLE = { index = 31, name = "JUMPIFNOTLE", has_aux = true },
    LOP_JUMPIFNOTLT = { index = 32, name = "JUMPIFNOTLT", has_aux = true },

    -- ADD, SUB, MUL, DIV, MOD, POW: compute arithmetic operation between two source registers and put the result into target register
    -- A: target register
    -- B: source register 1
    -- C: source register 2
    LOP_ADD = { index = 33, name = "ADD", has_aux = false },
    LOP_SUB = { index = 34, name = "SUB", has_aux = false },
    LOP_MUL = { index = 35, name = "MUL", has_aux = false },
    LOP_DIV = { index = 36, name = "DIV", has_aux = false },
    LOP_MOD = { index = 37, name = "MOD", has_aux = false },
    LOP_POW = { index = 38, name = "POW", has_aux = false },

    -- ADDK, SUBK, MULK, DIVK, MODK, POWK: compute arithmetic operation between the source register and a constant and put the result into target register
    -- A: target register
    -- B: source register
    -- C: constant table index (0..255); must refer to a number
    LOP_ADDK = { index = 39, name = "ADDK", has_aux = false },
    LOP_SUBK = { index = 40, name = "SUBK", has_aux = false },
    LOP_MULK = { index = 41, name = "MULK", has_aux = false },
    LOP_DIVK = { index = 42, name = "DIVK", has_aux = false },
    LOP_MODK = { index = 43, name = "MODK", has_aux = false },
    LOP_POWK = { index = 44, name = "POWK", has_aux = false },

    -- AND, OR: perform `and` or `or` operation (selecting first or second register based on whether the first one is truthy) and put the result into target register
    -- A: target register
    -- B: source register 1
    -- C: source register 2
    LOP_AND = { index = 45, name = "AND", has_aux = false },
    LOP_OR = { index = 46, name = "OR", has_aux = false },

    -- ANDK, ORK: perform `and` or `or` operation (selecting source register or constant based on whether the source register is truthy) and put the result into target register
    -- A: target register
    -- B: source register
    -- C: constant table index (0..255)
    LOP_ANDK = { index = 47, name = "ANDK", has_aux = false },
    LOP_ORK = { index = 48, name = "ORK", has_aux = false },

    -- CONCAT: concatenate all strings between B and C (inclusive) and put the result into A
    -- A: target register
    -- B: source register start
    -- C: source register end
    LOP_CONCAT = { index = 49, name = "CONCAT", has_aux = false },

    -- NOT, MINUS, LENGTH: compute unary operation for source register and put the result into target register
    -- A: target register
    -- B: source register
    LOP_NOT = { index = 50, name = "NOT", has_aux = false },
    LOP_MINUS = { index = 51, name = "MINUS", has_aux = false },
    LOP_LENGTH = { index = 52, name = "LENGTH", has_aux = false },

    -- NEWTABLE: create table in target register
    -- A: target register
    -- B: table size, stored as 0 for v=0 and ceil(log2(v))+1 for v!=0
    -- AUX: array size
    LOP_NEWTABLE = { index = 53, name = "NEWTABLE", has_aux = true },

    -- DUPTABLE: duplicate table using the constant table template to target register
    -- A: target register
    -- D: constant table index (0..32767)
    LOP_DUPTABLE = { index = 54, name = "DUPTABLE", has_aux = false },

    -- SETLIST: set a list of values to table in target register
    -- A: target register
    -- B: source register start
    -- C: value count + 1, or 0 to use all values up to top (MULTRET)
    -- AUX: table index to start from
    LOP_SETLIST = { index = 55, name = "SETLIST", has_aux = true },

    -- FORNPREP: prepare a numeric for loop, jump over the loop if first iteration doesn't need to run
    -- A: target register; numeric for loops assume a register layout [limit, step, index, variable]
    -- D: jump offset (-32768..32767)
    -- limit/step are immutable, index isn't visible to user code since it's copied into variable
    LOP_FORNPREP = { index = 56, name = "FORNPREP", has_aux = false },

    -- FORNLOOP: adjust loop variables for one iteration, jump back to the loop header if loop needs to continue
    -- A: target register; see FORNPREP for register layout
    -- D: jump offset (-32768..32767)
    LOP_FORNLOOP = { index = 57, name = "FORNLOOP", has_aux = false },

    -- FORGLOOP: adjust loop variables for one iteration of a generic for loop, jump back to the loop header if loop needs to continue
    -- A: target register; generic for loops assume a register layout [generator, state, index, variables...]
    -- D: jump offset (-32768..32767)
    -- AUX: variable count (1..255) in the low 8 bits, high bit indicates whether to use ipairs-style traversal in the fast path
    -- loop variables are adjusted by calling generator(state, index) and expecting it to return a tuple that's copied to the user variables
    -- the first variable is then copied into index; generator/state are immutable, index isn't visible to user code
    LOP_FORGLOOP = { index = 58, name = "FORGLOOP", has_aux = true },

    -- FORGPREP_INEXT: prepare FORGLOOP with 2 output variables (no AUX encoding), assuming generator is luaB_inext, and jump to FORGLOOP
    -- A: target register (see FORGLOOP for register layout)
    LOP_FORGPREP_INEXT = { index = 59, name = "FORGPREP_INEXT", has_aux = false },

    -- FASTCALL3: perform a fast call of a built-in function using 3 register arguments
    -- A: builtin function id (see LuauBuiltinFunction)
    -- B: source argument register
    -- C: jump offset to get to following CALL
    -- AUX: source register 2 in least-significant byte
    -- AUX: source register 3 in second least-significant byte
    LOP_FASTCALL3 = { index = 60, name = "FASTCALL3", has_aux = true },

    -- FORGPREP_NEXT: prepare FORGLOOP with 2 output variables (no AUX encoding), assuming generator is luaB_next, and jump to FORGLOOP
    -- A: target register (see FORGLOOP for register layout)
    LOP_FORGPREP_NEXT = { index = 61, name = "FORGPREP_NEXT", has_aux = false },

    -- NATIVECALL: start executing new function in native code
    -- this is a pseudo-instruction that is never emitted by bytecode compiler, but can be constructed at runtime to accelerate native code dispatch
    LOP_NATIVECALL = { index = 62, name = "NATIVECALL", has_aux = false },

    -- GETVARARGS: copy variables into the target register from vararg storage for current function
    -- A: target register
    -- B: variable count + 1, or 0 to copy all variables and adjust top (MULTRET)
    LOP_GETVARARGS = { index = 63, name = "GETVARARGS", has_aux = false },

    -- DUPCLOSURE: create closure from a pre-created function object (reusing it unless environments diverge)
    -- A: target register
    -- D: constant table index (0..32767)
    LOP_DUPCLOSURE = { index = 64, name = "DUPCLOSURE", has_aux = false },

    -- PREPVARARGS: prepare stack for variadic functions so that GETVARARGS works correctly
    -- A: number of fixed arguments
    LOP_PREPVARARGS = { index = 65, name = "PREPVARARGS", has_aux = false },

    -- LOADKX: sets register to an entry from the constant table from the proto (number/string)
    -- A: target register
    -- AUX: constant table index
    LOP_LOADKX = { index = 66, name = "LOADKX", has_aux = true },

    -- JUMPX: jumps to the target offset; like JUMPBACK, supports interruption
    -- E: jump offset (-2^23..2^23; 0 means "next instruction" aka "don't jump")
    LOP_JUMPX = { index = 67, name = "JUMPX", has_aux = false },

    -- FASTCALL: perform a fast call of a built-in function
    -- A: builtin function id (see LuauBuiltinFunction)
    -- C: jump offset to get to following CALL
    -- FASTCALL is followed by one of (GETIMPORT, MOVE, GETUPVAL) instructions and by CALL instruction
    -- This is necessary so that if FASTCALL can't perform the call inline, it can continue normal execution
    -- If FASTCALL *can* perform the call, it jumps over the instructions *and* over the next CALL
    -- Note that FASTCALL will read the actual call arguments, such as argument/result registers and counts, from the CALL instruction
    LOP_FASTCALL = { index = 68, name = "FASTCALL", has_aux = false },

    -- COVERAGE: update coverage information stored in the instruction
    -- E: hit count for the instruction (0..2^23-1)
    -- The hit count is incremented by VM every time the instruction is executed, and saturates at 2^23-1
    LOP_COVERAGE = { index = 69, name = "COVERAGE", has_aux = false },

    -- CAPTURE: capture a local or an upvalue as an upvalue into a newly created closure; only valid after NEWCLOSURE
    -- A: capture type, see LuauCaptureType
    -- B: source register (for VAL/REF) or upvalue index (for UPVAL/UPREF)
    LOP_CAPTURE = { index = 70, name = "CAPTURE", has_aux = false },

    -- SUBRK, DIVRK: compute arithmetic operation between the constant and a source register and put the result into target register
    -- A: target register
    -- B: constant table index (0..255); must refer to a number
    -- C: source register
    LOP_SUBRK = { index = 71, name = "SUBRK", has_aux = false },
    LOP_DIVRK = { index = 72, name = "DIVRK", has_aux = false },

    -- FASTCALL1: perform a fast call of a built-in function using 1 register argument
    -- A: builtin function id (see LuauBuiltinFunction)
    -- B: source argument register
    -- C: jump offset to get to following CALL
    LOP_FASTCALL1 = { index = 73, name = "FASTCALL1", has_aux = false },

    -- FASTCALL2: perform a fast call of a built-in function using 2 register arguments
    -- A: builtin function id (see LuauBuiltinFunction)
    -- B: source argument register
    -- C: jump offset to get to following CALL
    -- AUX: source register 2 in least-significant byte
    LOP_FASTCALL2 = { index = 74, name = "FASTCALL2", has_aux = true },

    -- FASTCALL2K: perform a fast call of a built-in function using 1 register argument and 1 constant argument
    -- A: builtin function id (see LuauBuiltinFunction)
    -- B: source argument register
    -- C: jump offset to get to following CALL
    -- AUX: constant index
    LOP_FASTCALL2K = { index = 75, name = "FASTCALL2K", has_aux = true },

    -- FORGPREP: prepare loop variables for a generic for loop, jump to the loop backedge unconditionally
    -- A: target register; generic for loops assume a register layout [generator, state, index, variables...]
    -- D: jump offset (-32768..32767)
    LOP_FORGPREP = { index = 76, name = "FORGPREP", has_aux = false },

    -- JUMPXEQKNIL, JUMPXEQKB: jumps to target offset if the comparison with constant is true (or false, see AUX)
    -- A: source register 1
    -- D: jump offset (-32768..32767; 1 means "next instruction" aka "don't jump")
    -- AUX: constant value (for boolean) in low bit, NOT flag (that flips comparison result) in high bit
    LOP_JUMPXEQKNIL = { index = 77, name = "JUMPXEQKNIL", has_aux = true },
    LOP_JUMPXEQKB = { index = 78, name = "JUMPXEQKB", has_aux = true },

    -- JUMPXEQKN, JUMPXEQKS: jumps to target offset if the comparison with constant is true (or false, see AUX)
    -- A: source register 1
    -- D: jump offset (-32768..32767; 1 means "next instruction" aka "don't jump")
    -- AUX: constant table index in low 24 bits, NOT flag (that flips comparison result) in high bit
    LOP_JUMPXEQKN = { index = 79, name = "JUMPXEQKN", has_aux = true },
    LOP_JUMPXEQKS = { index = 80, name = "JUMPXEQKS", has_aux = true },

    -- IDIV: compute floor division between two source registers and put the result into target register
    -- A: target register
    -- B: source register 1
    -- C: source register 2
    LOP_IDIV = { index = 81, name = "IDIV", has_aux = false },

    -- IDIVK compute floor division between the source register and a constant and put the result into target register
    -- A: target register
    -- B: source register
    -- C: constant table index (0..255)
    LOP_IDIVK = { index = 82, name = "IDIVK", has_aux = false },

    -- Enum entry for number of opcodes, not a valid opcode by itself!
    LOP__COUNT = { index = 83, name = "_COUNT", has_aux = false },
}

local LuauBytecodeTag = {
    LBC_VERSION_MIN = 3,
    LBC_VERSION_MAX = 6,

    LBC_TYPE_VERSION_MIN = 1,
    LBC_TYPE_VERSION_MAX = 3,
}

local LuauBytecodeConstants = {
    LBC_CONSTANT_NIL = 0,
    LBC_CONSTANT_BOOLEAN = 1,
    LBC_CONSTANT_NUMBER = 2,
    LBC_CONSTANT_STRING = 3,
    LBC_CONSTANT_IMPORT = 4,
    LBC_CONSTANT_TABLE = 5,
    LBC_CONSTANT_CLOSURE = 6,
    LBC_CONSTANT_VECTOR = 7,
}

local LuauProtoFlag = {
    -- used to tag main proto for modules with --!native
    LPF_NATIVE_MODULE = bit32.lshift(1, 0),
    -- used to tag individual protos as not profitable to compile natively
    LPF_NATIVE_COLD = bit32.lshift(1, 1),
    -- used to tag main proto for modules that have at least one function with native attribute
    LPF_NATIVE_FUNCTION = bit32.lshift(1, 2),
};

return {
    LuauBytecodeTag = table.freeze(LuauBytecodeTag),
    LuauBytecodeConstants = table.freeze(LuauBytecodeConstants),
    LuauOpcode = table.freeze(LuauOpcode),
    LuauProtoFlag = table.freeze(LuauProtoFlag)
}


local net = require("@lute/net")

local htmluau = require("../src")

local html = htmluau.html
local div = htmluau.div
local head = htmluau.head
local title = htmluau.title
local body = htmluau.body
local img = htmluau.img
local h2 = htmluau.h2
local p = htmluau.p

local createComponent = htmluau.createComponent

local Card = createComponent(function(props: { image: string, title: string, content: string })
	return div({ style = "display: flex; flex-direction: column; width: 200px" }) {
		img({ src = props.image, alt = props.title }),
		h2() {
			props.title,
		},
		p() {
			props.content,
		},
	}
end)

local website = html({ lang = "en" }) {
	head() {
		title() {
			"Card Example",
		},
	},
	body({ style = "font-family: sans-serif; margin: 0; padding: 20px" }) {
		div({ style = "display: flex; gap: 10px" }) {
			Card({
				image = "https://picsum.photos/id/237/200",
				title = "Card Title",
				content = "This is the content of the card.",
			}),
			Card({
				image = "https://picsum.photos/id/234/200",
				title = "Another Card Title",
				content = "This is the content of another card.",
			}),
		},
	},
}

net.serve(function()
	return website()
end)


local net = require("@lute/net")

local htmluau = require("../src")

local html = htmluau.html
local head = htmluau.head
local title = htmluau.title
local body = htmluau.body
local h1 = htmluau.h1
local button = htmluau.button
local p = htmluau.p

local website = html({ lang = "en" }) {
	head() {
		title() {
			"Hello World",
		},
	},
	body() {
		h1() {
			"Hello World",
		},
		button({ onclick = "alert('Hello World!')" }) {
			"Click Me",
		},
		p() {
			"This is a simple HTML page generated using Luau.",
			"Lines are concatenated.",
			"So you can write sentences on separate lines.",
			"<html> is properly excaped.",
		},
		button(),
	},
}

net.serve(function()
	return website()
end)


export type Node = string | () -> string -- Nodes are what user defined functions return
type Element = () -> string -- Elements are what Components return
type ChildlessElement = () -> Element -- ChildlessElements are when a component is called without children
type Child = string | Element | ChildlessElement
type Component<T> = (props: T) -> (children: { Child }?) -> Element

local function escapeHtml(attribute: string): string
	attribute = string.gsub(attribute, "&", "&")
	attribute = string.gsub(attribute, "<", "<")
	attribute = string.gsub(attribute, ">", ">")
	attribute = string.gsub(attribute, "'", "'")
	attribute = string.gsub(attribute, '"', """)
	return attribute
end

--[[
Wraps a two-argument function into a "component" nested function.

local Text = createComponent(function(props: { bold: boolean }, children: { string }?)
  local contents = table.concat(children, "\n")
  if props.bold then
    return strong() {
      contents
    }
  else
    return contents
  end
end)

local boldText = Text({ bold = true }) {
  "Hello World",
}

print(boldText()) -- Hello World
]]
--
local function createComponent<T>(component: (props: T, children: { string }?) -> Node): Component<T>
	return function(props: T)
		return function(children: { Child }?)
			-- Turn the Child array into a string array. Child may be an Element or Childless Element.
			local stringChildren: { string }?
			if children then
				stringChildren = {}
				for i, child in children do
					if typeof(child) == "function" then -- Is an Element or Childless Element
						-- Child may still be a function, this is when children haven't been passed:
						-- Component(props) {
						--   OtherComponent(), <-- OtherComponent to be unwrapped twice! Once to get the Element and once to get the string
						-- }
						local wrappedElement = child()
						if typeof(wrappedElement) == "function" then
							wrappedElement = wrappedElement()
						end
						stringChildren[i] = wrappedElement
					else
						-- Any raw strings passed in as children are escaped.
						stringChildren[i] = escapeHtml(child)
					end
				end
			end

			-- Return final Element
			return function()
				local result = component(props, stringChildren)
				if typeof(result) == "function" then
					return result()
				else
					return result
				end
			end
		end
	end
end

local function createHtmlElement(tag: string)
	local function htmlElement(props: { [string]: string }?, children: { string }?)
		local attributes = ""
		if props then
			for key, value in props do
				attributes ..= ` {key}="{escapeHtml(value)}"`
			end
		end

		if children then
			return `<{tag}{attributes}>{table.concat(children, "\n")}</{tag}>`
		else
			return `<{tag}{attributes}></{tag}>`
		end
	end

	return createComponent(htmlElement)
end

return {
	createComponent = createComponent,

	div = createHtmlElement("div"),
	span = createHtmlElement("span"),
	h1 = createHtmlElement("h1"),
	h2 = createHtmlElement("h2"),
	h3 = createHtmlElement("h3"),
	h4 = createHtmlElement("h4"),
	h5 = createHtmlElement("h5"),
	h6 = createHtmlElement("h6"),
	p = createHtmlElement("p"),
	a = createHtmlElement("a"),
	img = createHtmlElement("img"),
	button = createHtmlElement("button"),
	input = createHtmlElement("input"),
	label = createHtmlElement("label"),
	textarea = createHtmlElement("textarea"),
	select = createHtmlElement("select"),
	option = createHtmlElement("option"),
	ul = createHtmlElement("ul"),
	ol = createHtmlElement("ol"),
	li = createHtmlElement("li"),
	table = createHtmlElement("table"),
	tr = createHtmlElement("tr"),
	td = createHtmlElement("td"),
	th = createHtmlElement("th"),
	thead = createHtmlElement("thead"),
	tbody = createHtmlElement("tbody"),
	tfoot = createHtmlElement("tfoot"),
	form = createHtmlElement("form"),
	br = createHtmlElement("br"),
	hr = createHtmlElement("hr"),
	strong = createHtmlElement("strong"),
	b = createHtmlElement("b"),
	em = createHtmlElement("em"),
	i = createHtmlElement("i"),
	u = createHtmlElement("u"),
	s = createHtmlElement("s"),
	sup = createHtmlElement("sup"),
	sub = createHtmlElement("sub"),
	small = createHtmlElement("small"),
	code = createHtmlElement("code"),
	pre = createHtmlElement("pre"),
	blockquote = createHtmlElement("blockquote"),
	nav = createHtmlElement("nav"),
	header = createHtmlElement("header"),
	footer = createHtmlElement("footer"),
	section = createHtmlElement("section"),
	article = createHtmlElement("article"),
	aside = createHtmlElement("aside"),
	main = createHtmlElement("main"),
	details = createHtmlElement("details"),
	summary = createHtmlElement("summary"),
	dialog = createHtmlElement("dialog"),
	time = createHtmlElement("time"),
	address = createHtmlElement("address"),
	mark = createHtmlElement("mark"),
	progress = createHtmlElement("progress"),
	meter = createHtmlElement("meter"),
	caption = createHtmlElement("caption"),
	figure = createHtmlElement("figure"),
	figcaption = createHtmlElement("figcaption"),
	legend = createHtmlElement("legend"),
	fieldset = createHtmlElement("fieldset"),
	dfn = createHtmlElement("dfn"),
	kbd = createHtmlElement("kbd"),
	var = createHtmlElement("var"),
	cite = createHtmlElement("cite"),
	q = createHtmlElement("q"),

	html = createHtmlElement("html"),
	head = createHtmlElement("head"),
	title = createHtmlElement("title"),
	meta = createHtmlElement("meta"),
	link = createHtmlElement("link"),
	style = createHtmlElement("style"),
	body = createHtmlElement("body"),

	script = createHtmlElement("script"),
	noscript = createHtmlElement("noscript"),

	audio = createHtmlElement("audio"),
	video = createHtmlElement("video"),
	source = createHtmlElement("source"),
	track = createHtmlElement("track"),
	iframe = createHtmlElement("iframe"),
	canvas = createHtmlElement("canvas"),
	svg = createHtmlElement("svg"),
}


local chrome = require("../src")

local browser = chrome.browser.create()
local tab = browser:create_tab()

tab:navigate_to("https://www.wikipedia.org"):wait_until_navigated()

local item = tab:get_storage("translationHash") :: string
print(item)

browser:close()


local process = require("@lune/process")
local fs = require("@lune/fs")
local chrome = require("../src")

local WEBSOCKET_URL = assert(process.args[1], "Must provide websocket url")
local FILE_PATH = assert(process.args[2], "Must provide path/to/file/index.html")

local browser = chrome.browser.connect(WEBSOCKET_URL)
local tab = browser:create_tab()

local wikidata = tab:navigate_to("https://www.wikipedia.org"):wait_until_navigated():print_to_pdf()
fs.writeFile("wiki.pdf", wikidata)
print("PDF successfully created from internet web page.")

local local_pdf = tab:navigate_to(FILE_PATH):wait_until_navigated():print_to_pdf()
fs.writeFile("rust.pdf", local_pdf)
print("PDF successfully created from local web page.")


local chrome = require("../src")

local browser = chrome.browser.create({
	-- The searchbar won't appear if the resolution is too low
	window_size = { width = 1600, height = 768 },
})

local function query(input: string)
	local tab = browser:create_tab()

	tab:navigate_to("https://en.wikipedia.org")
		:wait_until_navigated()
		:wait_for_element("input#searchInput")
		:click()
		:type_into(input)
	tab:press_key("Enter"):wait_until_navigated()

	local short_description = tab:find_element("div.shortdescription")
	if not short_description then
		print("No shortdescription-node found on page")
		return
	end

	print(`Result for {input}: {short_description:get_inner_text()}`)

	tab:close()
end

query("Google Chrome")
query("WebKit")
query("Amlou")

browser:close()


local chrome = require("../src")

local browser = chrome.browser.create({ headless = false })
local tab = browser:create_tab()

tab:navigate_to("https://www.google.com"):wait_until_navigated()

tab:add_event_listener(chrome.Event.PageLifecycleEvent, function(lifecycle)
	if lifecycle.name == "DOMContentLoaded" then
		print(tab:get_url())
	end
end)


local fs = require("@lune/fs")
local chrome = require("../src")

local browser = chrome.browser.create()
local tab = browser:create_tab()

local jpeg = tab:navigate_to("https://www.wikipedia.org")
	:wait_until_navigated()
	:capture_screenshot({ format = "jpeg", quality = 75, from_surface = true })
fs.writeFile("screenshot.jpg", jpeg)

local png = tab:navigate_to("https://en.wikipedia.org/wiki/WebKit")
	:wait_until_navigated()
	:wait_for_element("#mw-content-text > div > table.infobox.vevent")
	:capture_screenshot({ format = "png", from_surface = true })
fs.writeFile("screenshot.png", png)

print("Screenshots successfully created.")

browser:close()


local types = require("./types")

export type Context = types.Context

local function get_id(context: Context): string
	return context.id
end

local function get_tabs(context: Context): { types.Tab }
	local tabs = {}

	for _, t in context.browser:get_tabs() do
		if t:get_browser_context_id() == context.id then
			table.insert(tabs, t)
		end
	end

	return tabs
end

local function create_tab(context: Context): types.Tab
	return context.browser:create_tab({
		url = "about:blank",
		browser_context_id = context.id,
	})
end

local function create(browser: types.Browser, context_id: string): Context
	return {
		id = context_id,
		browser = browser,
		get_id = get_id,
		get_tabs = get_tabs,
		create_tab = create_tab,
	}
end

return { create = create }


local proc = require("@self/proc")
local browser = require("@self/browser")
local context = require("@self/context")
local tab = require("@self/tab")
local polling = require("@self/util/polling")
local Method = require("@self/protocol/method")
local Event = require("@self/protocol/event")

export type LaunchOptions = proc.LaunchOptions
export type Browser = browser.Browser
export type Context = context.Context

export type Tab = tab.Tab
export type Element = tab.Element
export type ElementQuad = tab.ElementQuad
export type BoxModel = tab.BoxModel

return {
	polling = polling,
	browser = browser,
	Method = Method,
	Event = Event,
}


local process = require("@lune/process")
local fs = require("@lune/fs")
local net = require("@lune/net")
local Regex = require("@lune/regex")
local tempfile = require("./util/tempfile")
local winreg = require("./util/winreg")

--[=[
	Represents the way in which Chrome is run. By default it will search for a Chrome
	binary on the system, use an available port for debugging, and start in headless mode.

	@interface LaunchOptions
	.headless boolean? -- Determines whether to run headless version of the browser. Defaults to true.
	.sandbox boolean? -- Determines whether to run the browser with a sandbox.
	.devtools boolean? -- Automatically open devtools for tabs. Forces headless to be false.
	.enable_gpu boolean? -- Determines whether to enable GPU or not. Default to false.
	.enable_logging boolean? -- Determines whether to run the browser with logging enabled (this can cause unwanted new shell window in Windows 10 and above).
	.window_size { width: number, height: number }? -- Launch the browser with a specific window width and height.
	.port number? -- Launch the browser with a specific debugging port.
	.ignore_certificate_errors boolean? -- Determines whether SSL certificates should be verified. This is unsafe and can lead to MiTM attacks. This is unsafe and can lead to MiTM attacks. Make sure you understand the risks.
	.path string? -- Path for Chrome or Chromium. If unspecified, the create will try to automatically detect a suitable binary.
	.user_data_dir string? -- User Data (Profile) to use. If unspecified, a new temp directory is created and used on every launch.
	.extensions { string }? -- A list of Chrome extensions to load. An extension should be a path to a folder containing the extension code. CRX files cannot be used directly and must be first extracted. Note that Chrome does not support loading extensions in headless-mode.
	.args { string }? -- Additional arguments to pass to the browser instance. The list of Chromium flags can be found: <http://peter.sh/experiments/chromium-command-line-switches/>.
	.ignore_default_args { string }? -- Ignore a default given flag.
	.disable_default_args boolean? -- Disable default arguments.
	.idle_browser_timeout number? -- How long to keep the WebSocket to the browser for after not receiving any events from it. Defaults to 30 seconds.
	.process_envs { [string]: string }? -- Environment variables to set for the Chromium process.
	.proxy_server string? -- Setup the proxy server for headless chrome instance
	@within Process
]=]
export type LaunchOptions = {
	headless: boolean?,
	sandbox: boolean?,
	devtools: boolean?,
	enable_gpu: boolean?,
	enable_logging: boolean?,
	window_size: { width: number, height: number }?,
	port: number?,
	ignore_certificate_errors: boolean?,
	path: string?,
	user_data_dir: string?,
	extensions: { string }?,
	args: { string }?,
	ignore_default_args: { string }?,
	disable_default_args: boolean?,
	-- fetcher_options
	idle_browser_timeout: number?,
	process_envs: { [string]: string }?,
	proxy_server: string?,
}

--- @class Process
export type Process = {
	child: process.ChildProcess,
	websocket_url: string,
	temp_user_data_dir: string?,
}

local DEFAULT_ARGS = table.freeze({
	"--disable-background-networking",
	"--enable-features=NetworkService,NetworkServiceInProcess",
	"--disable-background-timer-throttling",
	"--disable-backgrounding-occluded-windows",
	"--disable-breakpad",
	"--disable-client-side-phishing-detection",
	"--disable-component-extensions-with-background-pages",
	"--disable-default-apps",
	"--disable-dev-shm-usage",
	"--disable-extensions",
	-- BlinkGenPropertyTrees disabled due to crbug.com/937609
	"--disable-features=TranslateUI,BlinkGenPropertyTrees",
	"--disable-hang-monitor",
	"--disable-ipc-flooding-protection",
	"--disable-popup-blocking",
	"--disable-prompt-on-repost",
	"--disable-renderer-backgrounding",
	"--disable-sync",
	"--force-color-profile=srgb",
	"--metrics-recording-only",
	"--no-first-run",
	"--enable-automation",
	"--password-store=basic",
	"--use-mock-keychain",
})

local PORT_TAKEN_PATTERN = Regex.new("ERROR.*bind\\(\\)")
local WEBSOCKET_URL_PATTERN = Regex.new("listening on (.*/devtools/browser/.*)$")

local function which(binary_name: string): string?
	local path = process.env.PATH or process.env.Path
	if not path then
		return nil
	end

	for _, dir in string.split(path, ";") do
		if not fs.isDir(dir) then
			continue
		end

		for _, file in fs.readDir(dir) do
			local is_match = if process.os == "windows" then file == `{binary_name}.exe` else file == binary_name
			if is_match then
				return `{dir}/{file}`
			end
		end
	end

	return nil
end

local function get_chrome_path_from_registry(): string?
	local ok, path = pcall(function()
		return winreg
			.predef("HKEY_LOCAL_MACHINE")
			:open_subkey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe")
			:get_value()
	end)
	return if ok then path else nil
end

local function default_executable(): string
	local env_path = process.env.CHROME
	if env_path and fs.isFile(env_path) then
		return env_path
	end

	for _, app in
		{
			"google-chrome-stable",
			"google-chrome-beta",
			"google-chrome-dev",
			"google-chrome-unstable",
			"chromium",
			"chromium-browser",
			"microsoft-edge-stable",
			"microsoft-edge-beta",
			"microsoft-edge-dev",
			"chrome",
			"chrome-browser",
			"msedge",
			"microsoft-edge",
		}
	do
		local path = which(app)
		if path then
			return path
		end
	end

	if process.os == "macos" then
		for _, path in
			{
				"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
				"/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta",
				"/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev",
				"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
				"/Applications/Chromium.app/Contents/MacOS/Chromium",
				"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
				"/Applications/Microsoft Edge Beta.app/Contents/MacOS/Microsoft Edge Beta",
				"/Applications/Microsoft Edge Dev.app/Contents/MacOS/Microsoft Edge Dev",
				"/Applications/Microsoft Edge Canary.app/Contents/MacOS/Microsoft Edge Canary",
			}
		do
			if fs.isFile(path) then
				return path
			end
		end
	end

	if process.os == "windows" then
		local winreg_path = get_chrome_path_from_registry()
		if winreg_path and fs.isFile(winreg_path) then
			return winreg_path
		end

		local ms_edge_path = "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"
		if fs.isFile(ms_edge_path) then
			return ms_edge_path
		end
	end

	error("Could not auto detect a chrome executable")
end

local function is_port_available(port: number): boolean
	local available, handle = pcall(function()
		return net.serve(port, function()
			return { status = 200 }
		end)
	end)

	if available then
		handle.stop()
	end

	return available
end

local function get_available_port(): number
	for _ = 1, 1000 do
		local port = math.random(8000, 9000)
		if is_port_available(port) then
			return port
		end
	end

	error("Failed to find an available port")
end

local function get_websocket_url_from_output(child: process.ChildProcess): string
	local root_sandbox = "Running as root without --no-sandbox is not supported"

	local function extract(text: string): string?
		local captures = WEBSOCKET_URL_PATTERN:captures(text)
		if not captures then
			return nil
		end
		local capture = captures:get(1)
		return capture and capture.text
	end

	local stderr = child.stderr:read() :: string
	for line in stderr:gmatch("([^\n]+)") do
		if string.find(line, root_sandbox) then
			error("Running as root with no sandbox")
		end

		if PORT_TAKEN_PATTERN:isMatch(line) then
			error("Debug port in use")
		end

		local answer = extract(line)
		if answer then
			return answer
		end
	end

	error("Port open timeout")
end

local function start_process(launch_options: LaunchOptions): (process.ChildProcess, string?)
	if not launch_options.port then
		error("Port is required")
	end

	local args = {
		`--remote-debugging-port={launch_options.port}`,
		"--verbose",
		"--log-level=0",
		"--no-first-run",
	}

	local temp_user_data_dir: string?
	if launch_options.user_data_dir then
		table.insert(args, `--user-data-dir={launch_options.user_data_dir}`)
	else
		local user_data_dir = tempfile("chrome-luau-profile")
		table.insert(args, `--user-data-dir={user_data_dir}`)
		temp_user_data_dir = user_data_dir
	end

	if launch_options.disable_default_args ~= true then
		local defaults = table.clone(DEFAULT_ARGS)

		if launch_options.ignore_default_args then
			for _, arg in launch_options.ignore_default_args do
				local index = table.find(defaults, arg)
				table.remove(defaults, index)
			end
		end

		for _, arg in defaults do
			table.insert(args, arg)
		end
	end

	if launch_options.args then
		for _, arg in launch_options.args do
			table.insert(args, arg)
		end
	end

	if launch_options.window_size then
		table.insert(args, `--window-size={launch_options.window_size.width},{launch_options.window_size.height}`)
	end

	if launch_options.headless and not launch_options.devtools then
		table.insert(args, "--headless")
	elseif launch_options.devtools then
		table.insert(args, "--auto-open-devtools-for-tabs")
	end

	if launch_options.ignore_certificate_errors then
		table.insert(args, "--ignore-certificate-errors")
	end

	if launch_options.enable_logging then
		table.insert(args, "--enable-logging")
	end

	if launch_options.enable_gpu then
		table.insert(args, "--enable-gpu")
	end

	if launch_options.proxy_server then
		table.insert(args, `--proxy-server={launch_options.proxy_server}`)
	end

	if launch_options.sandbox == false then
		table.insert(args, "--no-sandbox")
		table.insert(args, "--disable-setuid-sandbox")
	end

	if launch_options.extensions then
		for _, extension in launch_options.extensions do
			table.insert(args, `--load-extension={extension}`)
		end
	end

	if not launch_options.path then
		error("Path is required")
	end

	return process.create(launch_options.path, args, {
		shell = true,
		env = launch_options.process_envs,
	}),
		temp_user_data_dir
end

local function launch(partial_launch_options: LaunchOptions?): Process
	local launch_options: LaunchOptions = {
		headless = true,
		sandbox = false,
		devtools = false,
	}

	if partial_launch_options then
		for key, value in partial_launch_options do
			launch_options[key] = value
		end
	end

	if not launch_options.path then
		-- TODO: Fetch Chrome installation
		launch_options.path = default_executable()
	end

	if not launch_options.port then
		launch_options.port = get_available_port()
	end

	local child, temp_user_data_dir = start_process(launch_options)
	local websocket_url = get_websocket_url_from_output(child)
	return {
		child = child,
		websocket_url = websocket_url,
		temp_user_data_dir = temp_user_data_dir,
	}
end

return { launch = launch }


local types = require("../types")
local Page = require("../protocol/page")

export type BoxModel = types.BoxModel

local function content_viewport(box_model: BoxModel): Page.Viewport
	return {
		x = box_model.content.top_left.x,
		y = box_model.content.top_left.y,
		width = box_model.content:width(),
		height = box_model.content:height(),
		scale = 1,
	}
end

local function padding_viewport(box_model: BoxModel): Page.Viewport
	return {
		x = box_model.padding.top_left.x,
		y = box_model.padding.top_left.y,
		width = box_model.padding:width(),
		height = box_model.padding:height(),
		scale = 1,
	}
end

local function border_viewport(box_model: BoxModel): Page.Viewport
	return {
		x = box_model.border.top_left.x,
		y = box_model.border.top_left.y,
		width = box_model.border:width(),
		height = box_model.border:height(),
		scale = 1,
	}
end

local function margin_viewport(box_model: BoxModel): Page.Viewport
	return {
		x = box_model.margin.top_left.x,
		y = box_model.margin.top_left.y,
		width = box_model.margin:width(),
		height = box_model.margin:height(),
		scale = 1,
	}
end

--- @within BoxModel
local function create(box_model: {
	content: types.ElementQuad,
	padding: types.ElementQuad,
	border: types.ElementQuad,
	margin: types.ElementQuad,
	width: number,
	height: number,
}): BoxModel
	return {
		content = box_model.content,
		padding = box_model.padding,
		border = box_model.border,
		margin = box_model.margin,
		width = box_model.width,
		height = box_model.height,
		content_viewport = content_viewport,
		padding_viewport = padding_viewport,
		border_viewport = border_viewport,
		margin_viewport = margin_viewport,
	}
end

return { create = create }


local Transport = require("../transport")
local Page = require("../protocol/page")

export type Dialog = {
	session_id: string,
	transport: Transport.Transport,
	accept: (dialog: Dialog, text: string?) -> (),
	dismiss: (dialog: Dialog) -> (),
}

local function handle(dialog: Dialog, accept: boolean, text: string?)
	dialog.transport:call_method_on_target(
		Page.HandleJavaScriptDialog,
		{ accept = accept, prompt_text = text },
		dialog.session_id
	)
end

local function accept(dialog: Dialog, text: string?)
	handle(dialog, true, text)
end

local function dismiss(dialog: Dialog)
	handle(dialog, false)
end

local function create(session_id: string, transport: Transport.Transport): Dialog
	return {
		session_id = session_id,
		transport = transport,
		accept = accept,
		dismiss = dismiss,
	}
end

return { create = create }


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

export type ElementQuad = types.ElementQuad

local function height(element_quad: ElementQuad): number
	return element_quad.bottom_left.y - element_quad.top_left.y
end

local function width(element_quad: ElementQuad): number
	return element_quad.top_right.x - element_quad.top_left.x
end

local function aspect_ratio(element_quad: ElementQuad): number
	return element_quad:width() / element_quad:height()
end

local function most_left(element_quad: ElementQuad): number
	return math.min(
		element_quad.top_right.x,
		element_quad.top_left.x,
		element_quad.bottom_right.x,
		element_quad.bottom_left.x
	)
end

local function most_right(element_quad: ElementQuad): number
	return math.max(
		element_quad.top_right.x,
		element_quad.top_left.x,
		element_quad.bottom_right.x,
		element_quad.bottom_left.x
	)
end

local function most_top(element_quad: ElementQuad): number
	return math.min(
		element_quad.top_right.y,
		element_quad.top_left.y,
		element_quad.bottom_right.y,
		element_quad.bottom_left.y
	)
end

local function most_bottom(element_quad: ElementQuad): number
	return math.max(
		element_quad.top_right.y,
		element_quad.top_left.y,
		element_quad.bottom_right.y,
		element_quad.bottom_left.y
	)
end

local function strictly_above(element_quad: ElementQuad, other: ElementQuad): boolean
	return element_quad:most_bottom() < other:most_top()
end

local function above(element_quad: ElementQuad, other: ElementQuad): boolean
	return element_quad:most_bottom() <= other:most_top()
end

local function strictly_below(element_quad: ElementQuad, other: ElementQuad): boolean
	return element_quad:most_top() > other:most_bottom()
end

local function below(element_quad: ElementQuad, other: ElementQuad): boolean
	return element_quad:most_top() >= other:most_bottom()
end

local function strictly_left_of(element_quad: ElementQuad, other: ElementQuad): boolean
	return element_quad:most_right() < other:most_left()
end

local function left_of(element_quad: ElementQuad, other: ElementQuad): boolean
	return element_quad:most_right() <= other:most_left()
end

local function strictly_right_of(element_quad: ElementQuad, other: ElementQuad): boolean
	return element_quad:most_left() > other:most_right()
end

local function right_of(element_quad: ElementQuad, other: ElementQuad): boolean
	return element_quad:most_left() >= other:most_right()
end

local function within_horizontal_bounds_of(element_quad: ElementQuad, other: ElementQuad): boolean
	return element_quad:most_left() >= other:most_left() and element_quad:most_right() <= other:most_right()
end

local function within_vertical_bounds_of(element_quad: ElementQuad, other: ElementQuad): boolean
	return element_quad:most_top() >= other:most_top() and element_quad:most_bottom() <= other:most_bottom()
end

local function within_bounds_of(element_quad: ElementQuad, other: ElementQuad): boolean
	return element_quad:within_horizontal_bounds_of(other) and element_quad:within_vertical_bounds_of(other)
end

--- @within ElementQuad
local function from_raw_points(raw_quad: { number }): ElementQuad
	return {
		top_left = vector.create(raw_quad[1], raw_quad[2]),
		top_right = vector.create(raw_quad[3], raw_quad[4]),
		bottom_right = vector.create(raw_quad[5], raw_quad[6]),
		bottom_left = vector.create(raw_quad[7], raw_quad[8]),
		height = height,
		width = width,
		aspect_ratio = aspect_ratio,
		most_left = most_left,
		most_right = most_right,
		most_top = most_top,
		most_bottom = most_bottom,
		strictly_above = strictly_above,
		above = above,
		strictly_below = strictly_below,
		below = below,
		strictly_left_of = strictly_left_of,
		left_of = left_of,
		strictly_right_of = strictly_right_of,
		right_of = right_of,
		within_horizontal_bounds_of = within_horizontal_bounds_of,
		within_vertical_bounds_of = within_vertical_bounds_of,
		within_bounds_of = within_bounds_of,
	}
end

return { from_raw_points = from_raw_points }


local element_quad = require("./element-quad")
local box_model = require("./box-model")
local polling = require("../util/polling")
local types = require("../types")
local DOM = require("../protocol/dom")
local Runtime = require("../protocol/runtime")

export type Element = types.Element

local DEFAULT_TIMEOUT = 3

local create: (parent: types.Tab, node_id: DOM.NodeId) -> Element?

local function extract_midpoint(remote_object: Runtime.RemoteObject): vector
	if not remote_object.preview then
		return vector.zero
	end

	local props = {}

	for _, property in remote_object.preview.properties do
		props[property.name] = tonumber(property.value) :: number
	end

	return vector.create(props.x + props.width / 2, props.y + props.height / 2)
end

local function get_description(element: Element): DOM.Node
	return element.parent:call_method(DOM.DescribeNode, { node_id = element.node_id, depth = 100 }).node
end

local function get_attributes(element: Element): { string }?
	local description = element:get_description()
	return description.attributes
end

local function get_attribute_value(element: Element, name: string): string?
	local expression = `function() \{ return this.getAttribute("{name}"); \}`

	local value = element:call_js_fn(expression, nil, true).value
	return value
end

local function get_inner_text(element: Element): string
	local text = element:call_js_fn("function() { return this.innerText }").value
	return text :: string
end

local function get_content(element: Element): string
	local html = element:call_js_fn("function() { return this.outerHTML }").value
	return html :: string
end

local function find_element(element: Element, selector: string): Element?
	return element.parent:run_query_selector_on_node(element.node_id, selector)
end

local function find_element_by_xpath(element: Element, query: string): Element?
	element.parent:get_document()

	local search =
		element.parent:call_method(DOM.PerformSearch, { query = query, include_user_agent_shadow_dom = true })
	if search.result_count <= 0 then
		return nil
	end

	local results = element.parent:call_method(DOM.GetSearchResults, {
		search_id = search.search_id,
		from_index = 0,
		to_index = search.result_count,
	})

	local node_id = results.node_ids[1]
	return create(element.parent, node_id)
end

local function find_elements(element: Element, selector: string): { Element }
	return element.parent:run_query_selector_all_on_node(element.node_id, selector)
end

local function find_elements_by_xpath(element: Element, query: string): { Element }
	element.parent:get_document()

	local search =
		element.parent:call_method(DOM.PerformSearch, { query = query, include_user_agent_shadow_dom = true })
	if search.result_count <= 0 then
		return {}
	end

	local results = element.parent:call_method(DOM.GetSearchResults, {
		search_id = search.search_id,
		from_index = 0,
		to_index = search.result_count,
	})

	local node_ids = results.node_ids
	local elements = {}

	for _, node_id in node_ids do
		local e = create(element.parent, node_id)
		if e then
			table.insert(elements, element)
		end
	end

	return elements
end

local function wait_for_element(element: Element, selector: string, timeout: number?): Element?
	local duration = timeout or DEFAULT_TIMEOUT
	return polling.with_timeout(duration):pause_until(function()
		return element:find_element(selector)
	end)
end

local function wait_for_xpath(element: Element, selector: string, timeout: number?): Element?
	local duration = timeout or DEFAULT_TIMEOUT
	return polling.with_timeout(duration):pause_until(function()
		return element:find_element_by_xpath(selector)
	end)
end

local function wait_for_elements(element: Element, selector: string, timeout: number?): { Element }
	local duration = timeout or DEFAULT_TIMEOUT
	return polling.with_timeout(duration):pause_until(function()
		local elements = element:find_elements(selector)
		return #elements > 0 and elements
	end) or {}
end

local function wait_for_elements_by_xpath(element: Element, selector: string, timeout: number?): { Element }
	local duration = timeout or DEFAULT_TIMEOUT
	return polling.with_timeout(duration):pause_until(function()
		local elements = element:find_elements_by_xpath(selector)
		return #elements > 0 and elements
	end) or {}
end

local function call_js_fn(
	element: Element,
	fn_declaration: string,
	args: { string }?,
	await_promise: boolean?
): Runtime.RemoteObject
	local call_args: { Runtime.CallArgument }

	if args then
		call_args = {}
		for index, arg in args do
			call_args[index] = { value = arg }
		end
	end

	return element.parent:call_method(Runtime.CallFunctionOn, {
		object_id = element.remote_object_id,
		function_declaration = fn_declaration,
		arguments = call_args,
		return_by_value = false,
		generate_preview = true,
		silent = false,
		await_promise = await_promise,
	}).result
end

local function get_midpoint(element: Element): vector
	local quads = element.parent:call_method(DOM.GetContentQuads, { backend_node_id = element.backend_node_id }).quads

	local raw_quad = quads[1]
	if raw_quad then
		local input_quad = element_quad.from_raw_points(raw_quad)
		return vector.create(
			(input_quad.bottom_right.x + input_quad.top_left.x) / 2,
			(input_quad.bottom_right.y + input_quad.top_left.y) / 2
		)
	end

	local midpoint = polling.with_timeout(20):pause_until(function()
		local result = element:call_js_fn([[
			function() {
                let rect = this.getBoundingClientRect();

                if(rect.x != 0) {
                    this.scrollIntoView();
                }

                return this.getBoundingClientRect();
            }
		]])

		local midpoint = extract_midpoint(result)
		if midpoint.x == 0 then
			return nil
		end

		return midpoint
	end)

	if not midpoint then
		error("Failed to get element midpoint")
	end
	return midpoint
end

local function get_box_model(element: Element): box_model.BoxModel
	local model = element.parent:call_method(DOM.GetBoxModel, { backend_node_id = element.backend_node_id }).model
	return box_model.create({
		content = element_quad.from_raw_points(model.content),
		padding = element_quad.from_raw_points(model.padding),
		border = element_quad.from_raw_points(model.border),
		margin = element_quad.from_raw_points(model.margin),
		width = model.width,
		height = model.height,
	})
end

local function scroll_into_view(element: Element): Element
	local result = element:call_js_fn(
		[[
			async function() {
            	if (!this.isConnected) return "Node is detached from document";
            	if (this.nodeType !== Node.ELEMENT_NODE) return "Node is not of type HTMLElement";

            	const visibleRatio = await new Promise(resolve => {
                	const observer = new IntersectionObserver(entries => {
                    	resolve(entries[0].intersectionRatio);
                    	observer.disconnect();
                	});
                	observer.observe(this);
            	});

            	if (visibleRatio !== 1.0)
                	this.scrollIntoView({
                    	block: "center",
                    	inline: "center",
                    	behavior: "instant",
                	});

            	return false;
        	}
	]],
		nil,
		true
	)

	if result.type == "string" then
		error(result.value)
	end

	return element
end

local function move_mouse_over(element: Element): Element
	element:scroll_into_view()
	local midpoint = element:get_midpoint()
	element.parent:move_mouse_to_point(midpoint)
	return element
end

local function click(element: Element): Element
	element:scroll_into_view()
	local midpoint = element:get_midpoint()
	element.parent:click_point(midpoint)
	return element
end

local function type_into(element: Element, text: string): Element
	element:click()
	element.parent:type_text(text)
	return element
end

local function focus(element: Element): Element
	element:scroll_into_view()
	element.parent:call_method(DOM.Focus, { backend_node_id = element.backend_node_id })
	return element
end

local function capture_screenshot(
	element: Element,
	options: {
		format: "jpeg" | "png" | "webp"?,
		quality: number?,
		from_surface: boolean?,
		optimize_for_speed: boolean?,
	}
): buffer
	element:scroll_into_view()
	return element.parent:capture_screenshot({
		format = options.format,
		quality = options.quality,
		clip = element:get_box_model():content_viewport(),
		from_surface = options.from_surface,
		optimize_for_speed = options.optimize_for_speed,
	})
end

--- Using a "node_id", of the type returned by QuerySelector and QuerySelectorAll, this finds
--- the "backend_node_id" and "remote_object_id" which are stable identifiers, unlike node_id.
--- We use these two when making various calls to the API because of that.
--- @within Element
function create(parent: types.Tab, node_id: DOM.NodeId): Element?
	if node_id == 0 then
		return nil
	end

	local node = parent:call_method(DOM.DescribeNode, { node_id = node_id, depth = 100 }).node

	local attributes = node.attributes
	local tag = node.node_name

	local backend_node_id = node.backend_node_id

	local object = parent:call_method(DOM.ResolveNode, { backend_node_id = backend_node_id }).object

	local value = tostring(object.value)
	local remote_object_id = object.object_id

	if not remote_object_id then
		error("Could not find object ID")
	end

	return {
		remote_object_id = remote_object_id,
		backend_node_id = backend_node_id,
		node_id = node_id,
		parent = parent,
		attributes = attributes,
		tag = tag,
		value = value,
		get_description = get_description,
		get_attributes = get_attributes,
		get_attribute_value = get_attribute_value,
		get_inner_text = get_inner_text,
		get_content = get_content,
		find_element = find_element,
		find_element_by_xpath = find_element_by_xpath,
		find_elements = find_elements,
		find_elements_by_xpath = find_elements_by_xpath,
		wait_for_element = wait_for_element :: any,
		wait_for_xpath = wait_for_xpath :: any,
		wait_for_elements = wait_for_elements,
		wait_for_elements_by_xpath = wait_for_elements_by_xpath,
		call_js_fn = call_js_fn,
		get_midpoint = get_midpoint,
		get_box_model = get_box_model,
		scroll_into_view = scroll_into_view,
		move_mouse_over = move_mouse_over,
		click = click,
		type_into = type_into,
		focus = focus,
		capture_screenshot = capture_screenshot,
	}
end

return { create = create }


local function transform(x: any, processor: (string) -> string): any
	if typeof(x) == "string" then
		return processor(x)
	elseif typeof(x) == "table" then
		local clone = {}

		for key, value in x do
			if typeof(value) == "table" then
				value = transform(value, processor)
			end
			clone[transform(key, processor)] = value
		end

		return clone
	else
		return x
	end
end

local function snake_to_camel<T>(value: T): T
	return transform(value, function(str)
		return string.gsub(str, "_(%l)", string.upper)
	end)
end

local function camel_to_snake<T>(value: T): T
	return transform(value, function(str)
		local result = str
		result = string.gsub(result, "(%u+)(%u%l)", "%1_%2")
		result = string.gsub(result, "([a-z%d])(%u)", "%1_%2")
		result = string.lower(result)
		return result
	end)
end

return {
	snake_to_camel = snake_to_camel,
	camel_to_snake = camel_to_snake,
}


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

export type Polling = {
	timeout: number,
	sleep: number,
	pause_until: <T>(self: Polling, try: () -> T?) -> T?,
}

local DEFAULT_TIMEOUT = 10
local DEFAULT_SLEEP = 1 / 10

local function pause_until<T>(polling: Polling, try: () -> T?): T?
	local start = os.clock()

	while true do
		local value = try()
		if value ~= nil then
			return value
		end

		if os.clock() - start > polling.timeout then
			return nil
		end

		task.wait(polling.sleep)
	end
end

local function create(timeout: number, sleep: number): Polling
	return {
		timeout = timeout,
		sleep = sleep,
		pause_until = pause_until,
	}
end

local function with_timeout(timeout: number): Polling
	return create(timeout, DEFAULT_SLEEP)
end

local function with_sleep(sleep: number): Polling
	return create(DEFAULT_TIMEOUT, sleep)
end

local function forever(): Polling
	return create(math.huge, DEFAULT_SLEEP)
end

return {
	create = create,
	with_timeout = with_timeout,
	with_sleep = with_sleep,
	forever = forever,
}


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

local NUM_RANDOM_CHARS = 6
local NUM_RETRIES = 2 ^ 31

local ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"

local function assert_temp(): string
	if not process.env.TEMP then
		error("Missing TEMP variable")
	end
	if not fs.isDir(process.env.TEMP) then
		error("TEMP variable is not a directory")
	end
	return process.env.TEMP
end

local function tempname(prefix: string, length: number): string
	local name = ""

	for _ = 1, length do
		local idx = math.random(1, #ALPHABET)
		local char = string.sub(ALPHABET, idx, idx)
		name ..= char
	end

	return prefix .. name
end

local function tempfile(prefix: string): string
	local temp = assert_temp()

	for _ = 0, NUM_RETRIES do
		local name = tempname(prefix, NUM_RANDOM_CHARS)
		local path = `{temp}/{name}`

		if not fs.isFile(path) and not fs.isDir(path) then
			return path
		end
	end

	error("Too many temporary files exist")
end

return tempfile


--[[
	MIT License

	Copyright (c) 2024-present jiwonz

	Permission is hereby granted, free of charge, to any person obtaining a copy
	of this software and associated documentation files (the "Software"), to deal
	in the Software without restriction, including without limitation the rights
	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	copies of the Software, and to permit persons to whom the Software is
	furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in all
	copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
	SOFTWARE.
]]

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

export type RegKey = {
	path: string,
	join_subkey: (self: RegKey, subpath: string) -> string,
	open_subkey: (self: RegKey, subpath: string) -> RegKey,
	get_value: (self: RegKey) -> string,
}

export type HkeyPath =
	| "HKEY_CLASSES_ROOT"
	| "HKEY_CURRENT_USER"
	| "HKEY_LOCAL_MACHINE"
	| "HKEY_USERS"
	| "HKEY_PERFORMANCE_DATA"
	| "HKEY_PERFORMANCE_TEXT"
	| "HKEY_PERFORMANCE_NLSTEXT"
	| "HKEY_CURRENT_CONFIG"
	| "HKEY_DYN_DATA"
	| "HKEY_CURRENT_USER_LOCAL_SETTINGS"

local ITEM_PATTERN =
	Regex.new("^(.*)\\s(REG_SZ|REG_MULTI_SZ|REG_EXPAND_SZ|REG_DWORD|REG_QWORD|REG_BINARY|REG_NONE)\\s+([^\\s].*)$")

local create_regkey: (path: string) -> RegKey

local function winreg_program(): string
	if process.os == "windows" then
		local windir = process.env.windir
		local path = `{windir}/system32/reg.exe`
		if fs.isFile(path) then
			return path
		end
	end
	return "REG"
end

local function winreg_exists(path: string): boolean
	local result = process.exec(winreg_program(), { "QUERY", `"{path}"`, "/ve" }, { shell = true })
	return result.ok
end

local function winreg_predef(hkey: HkeyPath): RegKey
	if winreg_exists(hkey) then
		return create_regkey(hkey)
	else
		error(`"{hkey}" is not a valid HKEY`)
	end
end

local function regkey_join_subkey(key: RegKey, subpath: string): string
	return `{key.path}\\{subpath}`
end

local function regkey_open_subkey(key: RegKey, subpath: string): RegKey
	local path = key:join_subkey(subpath)
	if winreg_exists(path) then
		return create_regkey(path)
	else
		error(`Key "{path}" does not exist`)
	end
end

local function regkey_get_raw_value(key: RegKey): { vtype: string, value: string }
	local result = process.exec(winreg_program(), { "QUERY", `"{key.path}"`, "/ve" }, { shell = true })
	if not result.ok then
		error(`Failed to read from key "{key.path}" ({result.stderr})`)
	end

	local items = {}
	local lines = string.split(result.stdout, "\r\n")

	for i = 2, #lines do
		local line = lines[i]
		if #line ~= 0 then
			table.insert(items, line)
		end
	end

	local item = items[#items] or ""
	local match = ITEM_PATTERN:captures(item)

	if match then
		local vtype = match:get(2)
		local value = match:get(3)
		if not vtype or not value then
			error("Failed to read from key")
		end

		return {
			vtype = vtype.text,
			value = value.text,
		}
	else
		error("Failed to capture item")
	end
end

local function regkey_get_value(key: RegKey): string
	local raw = regkey_get_raw_value(key)
	if raw.vtype == "REG_SZ" then
		return raw.value
	else
		error(`Unimplemented vtype "{raw.vtype}"`)
	end
end

function create_regkey(path: string): RegKey
	return {
		path = path,
		join_subkey = regkey_join_subkey,
		open_subkey = regkey_open_subkey,
		get_value = regkey_get_value,
	}
end

return {
	program = winreg_program,
	exists = winreg_exists,
	predef = winreg_predef,
}


// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from "vscode"
import { formatContent, formatFile } from "./format"

// This method is called when your extension is activated
// Your extension is activated the very first time the command is executed
async function format(
	document: vscode.TextDocument
): Promise<vscode.TextEdit[]> {
	vscode.window.showInformationMessage("Formatting document...")

	const config = vscode.workspace.getConfiguration("coputer")
	const tool = config.get<string>("tools.path")

	if (!tool) {
		vscode.window.showErrorMessage("Coputer tools path is not configured.")
		return []
	}

	// Disk files can be formatted directly, content files will need to be written to a temp file by the formatter
	const isDiskFile = document.uri.scheme === "file"
	let formatted: string
	try {
		formatted = isDiskFile
			? formatFile(tool, document.uri.fsPath)
			: await formatContent(tool, document.getText())
	} catch (err) {
		const e = err as Error
		vscode.window.showErrorMessage(
			`Failed to format document: ${e.message}`
		)
		return []
	}

	const end = document.lineAt(document.lineCount - 1)
	const replaced = vscode.TextEdit.replace(
		new vscode.Range(new vscode.Position(0, 0), end.range.end),
		formatted
	)
	return [replaced]
}

export function activate(context: vscode.ExtensionContext) {
	// Use the console to output diagnostic information (console.log) and errors (console.error)
	// This line of code will only be executed once when your extension is activated
	vscode.window.showInformationMessage("Coputer extension is now active")
	const disposable = vscode.languages.registerDocumentFormattingEditProvider(
		"luau",
		{ provideDocumentFormattingEdits: format }
	)

	context.subscriptions.push(disposable)
}

// This method is called when your extension is deactivated
export function deactivate() {}


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

local joinPath = require("@lune-lib/joinPath")

local function discover(root: string, extension: string?, exclusions: { string }?): { string }
	local discoveredFiles = {}

	local files = fs.readDir(root)

	for _, file in files do
		local fullPath = joinPath(root, file)

		if exclusions and table.find(exclusions, fullPath) then
			continue
		end

		if fs.isDir(fullPath) then
			local subPaths = discover(fullPath, extension)

			for _, v in subPaths do
				table.insert(discoveredFiles, v)
			end
		else
			if extension and file:sub(-#extension) ~= extension then
				continue
			end

			table.insert(discoveredFiles, fullPath)
		end
	end

	return discoveredFiles
end

return discover


local function joinPath(...): string
	return table.concat({ ... }, "/")
end

return joinPath


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

require("./license")()

local consoleReporter = require("@lune-lib/reporter")
consoleReporter.init()

local frktest = require("@frktest/frktest")
process.exit(if frktest.run() then 0 else 1)


local frktest = require("@frktest/frktest")
local test = frktest.test
local check = frktest.assert.check
local req = frktest.assert.require

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

local discover = require("@lune-lib/discover")

local EXCLUDE = {
	"./src/vendor",
}

local LICENSE_HEADER = fs.readFile("./include/license-header.txt")

local paths = discover("./src", "luau", EXCLUDE)

return function()
	test.suite("License checks", function()
		for _, file in paths do
			test.case(file, function()
				local src = fs.readFile(file)

				req.is_true(src:sub(1, #LICENSE_HEADER) == LICENSE_HEADER)

				return nil
			end)
		end

		return nil
	end)
end


--
-- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not
-- distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
--

-- whilst argparse has been patched to expose its types, pesde refuses to regenerate the linker script to expose them,
-- so we're doing some trickery here!

local argparse = require("@pkgs/.pesde/caveful_games+argparse/0.1.2/argparse/src/lib")

export type Argument = argparse.argument
export type Option = argparse.option
export type Flag = argparse.flag
export type Command = argparse.command
export type Parser = argparse.Parser

return argparse


--
-- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not
-- distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
--

local argparse = require("@cli/argparse")

return {
	require("./cmd_init"),
	--require("./run"),
	require("./update"),
} :: { (argparse.Parser) -> () }


--
-- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not
-- distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
--

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

local argparse = require("@cli/argparse")
local config = require("@cli/config")
local fmt = require("@cli/fmt")

local types = require("@lib/types")

local function findTransformer(name: string): string?
	-- is it a literal file path?
	if fs.isFile(name) then
		return name
	end

	-- are we just missing the luau extension?
	if fs.isFile(`{name}.luau`) then
		return `{name}.luau`
	end

	-- try to find in the bright folder
	local path = `./bright/transformers/{name}.luau`

	if fs.isFile(path) then
		return path
	end

	-- are we adding an extra luau extension?
	local pathNoExt = `./bright/transformers/{name}`

	if fs.isFile(pathNoExt) then
		return pathNoExt
	end

	-- no idea
	return nil
end

local function run(opts: { input: string, output: string, transformers: { string } })
	if #opts.transformers == 0 then
		opts.transformers = config.transformers
	end

	if not fs.isDir(opts.input) then
		fmt.bail(`Source path \`{opts.input}\` does not exist.`)
	end

	if #opts.transformers == 0 then
		fmt.bail("No transformers to run.", fmt.hint("Have you configured any transformers in `bright.toml`?"))
	end

	local transformerStack: { types.Transformer<any> } = {}

	for _, transformer in opts.transformers do
		local location = findTransformer(transformer)

		if not location then
			fmt.bail(`Could not find transformer \`{transformer}\`.`)
		end
	end
end

return function(parser: argparse.Parser)
	local command = parser:command("run"):description("Executes and applies transformers on source code"):action(run)

	command:argument("transformers"):description("The transformers to run"):args("*")

	command:option("-i --input"):description("The input source code"):default(config.source)

	command:option("-o --output"):description("The output for the transformed code"):default(config.output)
end


--
-- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not
-- distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
--

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

local argparse = require("@cli/argparse")
local constants = require("@cli/constants")
local fetchLibrariesFromGit = require("@cli/util/fetchLibrariesFromGit")
local fmt = require("@cli/fmt")

local function run()
	if fs.isFile("pesde.toml") then
		local pesdeToml = fs.readFile("pesde.toml")
		local pesde = serde.decode("toml", pesdeToml)

		if pesde.dev_dependencies and pesde.dev_dependencies.bright then
			fmt.bail(
				"Bright is managed through pesde!",
				fmt.hint("To update it, you need to run `pesde add -d bright/bright`.")
			)
		end
	end

	if not fs.isDir(`{constants.BRIGHT_FOLDER}/lib`) then
		fmt.bail("Bright is not installed!", fmt.hint("Run `bright init` to set up your project."))
	end

	fs.removeDir(`{constants.BRIGHT_FOLDER}/lib`)
	fs.writeDir(`{constants.BRIGHT_FOLDER}/lib`)

	local progress = fmt.progress("Updating Bright libraries", 1)

	fetchLibrariesFromGit(progress)

	progress:finish(fmt.symbols.success)
end

return function(parser: argparse.Parser)
	parser:command("update up"):description("Updates the current Bright installation"):action(run)
end


--
-- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not
-- distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
--

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

local constants = require("./constants")

type Config = {
	read source: string,
	read output: string,

	read transformers: { string },

	read rules: {
		[string]: {
			[string]: any,
		},
	},
}

local DEFAULT_CONFIG: Config = {
	source = "src/",
	output = "output/",

	transformers = {},
	rules = {},
}

local function readConfig(): Config
	if not fs.isFile(constants.CONFIGURATION_FILE) then
		return DEFAULT_CONFIG
	end

	local configFile = fs.readFile(constants.CONFIGURATION_FILE)

	local des = serde.decode("toml", configFile)

	-- all parameters are optional, so make sure they're all set
	for k, v in DEFAULT_CONFIG :: { [string]: any } do
		if des[k] == nil then
			des[k] = v
		end
	end

	return des :: Config
end

return readConfig()


--
-- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not
-- distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
--

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

local argparse = require("./argparse")
local cli = require("./cli")
local fmt = require("./fmt")

local bright = argparse()
	:name("bright")
	:description("A scriptable tool for transforming and transpiling Luau code.")
	:epilog("Visit https://brightluau.dev/ for further documentation.")

for i, subcommand in cli do
	subcommand(bright)
end

local ok, info = pcall(function()
	return bright:parse(process.args)
end)

if not ok then
	fmt.bail("Internal error occurred:", info)
end


--
-- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not
-- distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
--

return {
	std = require("./std"),
}


--
-- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not
-- distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
--

-- we use the "pokemore" version of the library, which means it uses a CST to preserve things like comments via trivia

local cst = require("@vendor/poke/pokemore/cst")
local parse = require("@vendor/poke/pokemore/parse")


--
-- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not
-- distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
--

local types = require("./types")

-- re-export all of the types that would be most used
export type Config<C> = types.Config<C>
export type NoConfig = types.NoConfig

--- Defines a transformer to be returned from the transformer module.
---
--- @param name string -- The name of the transformer. Used in output and for defining configuration.
--- @param config C -- A configuration object.
--- @param runner TransformerFunction -- The transformer itself.
---
--- @returns Transformer -- A transformer definition.
local function defineTransformer<C>(
	name: string,
	config: C,
	runner: types.TransformerFunction<types.Config<C>>
): types.Transformer<C>
	return {
		name = name,
		config = config,
		func = runner,
	}
end

return table.freeze {
	defineTransformer = defineTransformer,
}


--
-- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not
-- distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
--

--- Converts a configuration definition to a K/V pair of configuration options.
export type function Config(userConfig)
	-- if we get any, just return any
	if userConfig:is("any") then
		return userConfig
	end

	-- if we get unknown, this is usually a typing issue, so give a hint
	if userConfig:is("unknown") then
		error("param is unknown"
			.. "\n[bright] This commonly occurs when defining a transformer. Add a type to your transformer's second argument.", 0)
	end

	if not userConfig:is("table") then
		error(`param must be a table, is actually {userConfig.tag}`, 0)
	end

	-- if there are no properties in the config table, then the config will never be passed to the transformer, so just
	-- return never
	if not next(userConfig:properties()) then
		return types.never
	end

	local config = types.newtable()

	for optionName, v in userConfig:properties() do
		local optionDef = v.read

		if not optionDef:is("table") then
			error(`{optionName:value()} is not a valid option definition`
				.. `\n[bright] You are probably trying to do \`{optionName:value()} = ...\`, which is not valid. `
				.. `Create an option definition like so: \`{optionName:value()} = \{ default = ... }\`.`, 0)
		end

		local defaultDefinition = optionDef:readproperty(types.singleton("default"))

		if not defaultDefinition then
			error(`{optionName:value()} is missing a default value`, 0)
		end

		if defaultDefinition:is("negation") then
			error(`{optionName:value()} cannot be a negation`, 0)
		end

		if defaultDefinition:is("union") or defaultDefinition:is("intersection") then
			error(`{optionName:value()} cannot be a {defaultDefinition.tag}`
				.. `\n[bright] This type is currently not supported by the runtime type checker.`, 0)
		end

		if defaultDefinition:is("singleton") then
			error(`{optionName:value()} cannot be a singleton`
					.. "\n[bright] A singleton does not make sense in the context of a configuration. Do you need to assert "
					.. "the type?", 0)
		end

		config:setreadproperty(optionName, defaultDefinition)
	end

	return config
end

--- A blank configuration.
export type NoConfig = Config<{}>

export type TransformerFunction<C> = (string, config: C) -> string

export type Transformer<C> = {
	name: string,
	config: C,
	func: TransformerFunction<Config<C>>,
}

export type TransformerConfigOption<T> = {
	description: string,
	default: T,
}

export type TransformerConfig = {
	[string]: TransformerConfigOption<any>
}

return nil


local ESCAPE = string.char(27)
local ANSI_16 = `{ESCAPE}[%dm`

export type Styler = (text: string) -> string

local function createStylerFunction(opener: number, closer: number): Styler
	local open = string.format(ANSI_16, opener)
	local close = string.format(ANSI_16, closer)

	return function(text: string)
		return `{open}{text}{close}`
	end
end

local function combineStyles(styleArray: { (text: string) -> string }): Styler
	return function(text: string)
		local result = text

		for _, style in styleArray do
			result = style(result)
		end

		return result
	end
end

return {
	combineStyles = combineStyles,

	modifier = {
		dim = createStylerFunction(2, 22),
		reset = createStylerFunction(0, 0),
		bold = createStylerFunction(1, 22),
		italic = createStylerFunction(3, 23),
		hidden = createStylerFunction(8, 28),
		inverse = createStylerFunction(7, 27),
		underline = createStylerFunction(4, 24),
		overline = createStylerFunction(53, 55),
		strikethrough = createStylerFunction(9, 29),
	},

	color = {
		red = createStylerFunction(31, 39),
		blue = createStylerFunction(34, 39),
		cyan = createStylerFunction(36, 39),
		gray = createStylerFunction(90, 39),
		grey = createStylerFunction(90, 39),
		black = createStylerFunction(30, 39),
		green = createStylerFunction(32, 39),
		white = createStylerFunction(37, 39),
		yellow = createStylerFunction(33, 39),
		magenta = createStylerFunction(35, 39),
		redBright = createStylerFunction(91, 39),
		blueBright = createStylerFunction(94, 39),
		cyanBright = createStylerFunction(96, 39),
		blackBright = createStylerFunction(90, 39),
		greenBright = createStylerFunction(92, 39),
		whiteBright = createStylerFunction(97, 39),
		yellowBright = createStylerFunction(93, 39),
		magentaBright = createStylerFunction(95, 39),
	},
}


--!native
--!strict

local MAX_ITERATIONS: number = 256

local function getABV(
	originA: CFrame, marginA: number, invA: CFrame, localSupportA: any,
	originB: CFrame, marginB: number, invB: CFrame, localSupportB: any,
	n: Vector3
): (Vector3, Vector3, Vector3)
	local m = n:Dot(n) do
		if m > 0 then
			n /= m^0.5
		end
	end

	local SA, SB = originA*localSupportA(invA*n) + n*marginA, originB*localSupportB(invB*(-n)) - n*marginB
	return SA, SB, SA - SB
end

local function fastLinearConvexCast(
	originA: CFrame, marginA: number, localSupportA: any, directionA: Vector3,
	originB: CFrame, marginB: number, localSupportB: any, directionB: Vector3
): (number?, Vector3?, Vector3?)
	local rayDir = directionA - directionB
	if rayDir:Dot(rayDir) == 0 then return end

	local invA, invB = originA.Rotation:Inverse(), originB.Rotation:Inverse()

	local SA0, SB0, V0 = getABV(originA, marginA, invA, localSupportA, originB, marginB, invB, localSupportB, originA.Position - originB.Position)
	V0 = V0:Dot(V0) < 1e-5 and Vector3.yAxis or V0

	local SA1, SB1, V1 = getABV(originA, marginA, invA, localSupportA, originB, marginB, invB, localSupportB, rayDir:Cross(rayDir:Cross(V0)))
	if V0:Dot(V1:Cross(rayDir)) > 0 then
		SA0, SB0, V0, SA1, SB1, V1 = SA1, SB1, V1, SA0, SB0, V0
	end

	local lasti, lastn = nil, nil
	local SA2, SB2, V2 do
		for _ = 0, MAX_ITERATIONS do
			local n = (V1 - V0):Cross(rayDir)

			SA2, SB2, V2 = getABV(originA, marginA, invA, localSupportA, originB, marginB, invB, localSupportB, n)
			if n:Dot(V2) < 0 then return end

			local uv = V2:Cross(rayDir)
			if V0:Dot(uv) < 0 then
				SA1, SB1, V1 = SA2, SB2, V2
			elseif V1:Dot(uv) > 0 then
				SA0, SB0, V0 = SA2, SB2, V2
			else
				break
			end
		end
	end

	local SA3, SB3, V3 do
		local BestAlignment = 1/0
		for I = 0, MAX_ITERATIONS do
			local n = (V2 - V0):Cross(V1 - V0)

			SA3, SB3, V3 = getABV(originA, marginA, invA, localSupportA, originB, marginB, invB, localSupportB, n)

			local Alignment = (V3 - V0):Dot(n)
			if Alignment < 1e-6*I or math.abs(Alignment - BestAlignment) < 1e-12 then
				local t = -V0:Dot(n)/rayDir:Dot(n)

				local rd = rayDir*t
				local s0, s1, s2 = V0 + rd, V1 + rd, V2 + rd
				local u, v, w, x = s0:Cross(s1):Dot(s2), s2:Cross(s1):Dot(rayDir), rayDir:Cross(s0):Dot(s2), s1:Cross(s0):Dot(rayDir)

				local s = u + v + w + x
				if s <= 0 then
					u, v, w, x = 0, s1:Cross(s2):Dot(n), s2:Cross(s0):Dot(n), s0:Cross(s1):Dot(n)
					s = v + w + x
				end

				return t, (u*originB.Position + v*SB0 + w*SB1 + x*SB2)/s, -n.Unit
			end

			BestAlignment = Alignment

			local uv = V3:Cross(rayDir)
			if V0:Dot(uv) > 0 then
				if V1:Dot(uv) > 0 then
					SA0, SB0, V0 = SA3, SB3, V3
				else
					SA2, SB2, V2 = SA3, SB3, V3
				end
			else
				if V2:Dot(uv) > 0 then
					SA1, SB1, V1 = SA3, SB3, V3
				else
					SA0, SB0, V0 = SA3, SB3, V3
				end
			end
		end
	end

	return
end

return fastLinearConvexCast


--!strict

type Ball = {
	Shape: string,
	Margin: number,

	Transform: CFrame,
	Radius: number,

	fromPart: (basePart: BasePart) -> Ball,
	localSupport: (n: Vector3) -> Vector3
}

return function(): Ball
	local self: Ball = {
		Shape = 'Ball',
		Margin = 0,

		Transform = CFrame.identity,
		Radius = 0
	} :: Ball

	function self.fromPart(basePart)
		self.Transform = basePart.CFrame
		self.Radius = .5*math.min(basePart.Size.X, basePart.Size.Y, basePart.Size.Z)

		return self
	end

	@native function self.localSupport(n)
		return n.Unit*self.Radius
	end

	return self
end


--!strict

type Block = {
	Shape: string,
	Margin: number,

	Transform: CFrame,
	Extent: Vector3,

	fromPart: (basePart: BasePart) -> Block,
	localSupport: (n: Vector3) -> Vector3
}

return function(): Block
	local self: Block = {
		Shape = 'Block',
		Margin = 0,

		Transform = CFrame.identity,
		Extent = Vector3.zero
	} :: Block

	function self.fromPart(basePart)
		self.Transform = basePart.CFrame
		self.Extent = .5*basePart.Size

		return self
	end

	@native function self.localSupport(n)
		return Vector3.new(
			n.X < 0 and -self.Extent.X or self.Extent.X,
			n.Y < 0 and -self.Extent.Y or self.Extent.Y,
			n.Z < 0 and -self.Extent.Z or self.Extent.Z
		)
	end

	return self
end


--!strict

type CornerWedge = {
	Shape: string,
	Margin: number,

	Transform: CFrame,
	Extent: Vector3,

	fromPart: (basePart: BasePart) -> CornerWedge,
	localSupport: (n: Vector3) -> Vector3
}

local _vertices: {Vector3} = {
	Vector3.new(1,  1, -1), Vector3.new(-1, -1,  1),
	Vector3.new(1, -1, -1), Vector3.new(-1, -1, -1),
	Vector3.new(1, -1,  1)
}

local _verticesCount: number = #_vertices

return function(): CornerWedge
	local self: CornerWedge = {
		Shape = 'CornerWedge',
		Margin = 0,

		Transform = CFrame.identity,
		Extent = Vector3.zero
	} :: CornerWedge

	function self.fromPart(basePart)
		self.Transform = basePart.CFrame
		self.Extent = .5*basePart.Size

		return self
	end

	@native function self.localSupport(n)
		local curP, bestD = _vertices[1], n:Dot(_vertices[1]) do
			for I = 2, _verticesCount do
				local d = n:Dot(_vertices[I])
				if d > bestD then
					curP, bestD = _vertices[I], d
				end
			end
		end

		return curP*self.Extent
	end

	return self
end


--!strict

type Cylinder = {
	Shape: string,
	Margin: number,

	Transform: CFrame,
	Height: number,
	Radius: number,

	fromPart: (basePart: BasePart) -> Cylinder,
	localSupport: (n: Vector3) -> Vector3
}

return function(): Cylinder
	local self: Cylinder = {
		Shape = 'Cylinder',
		Margin = 0,

		Transform = CFrame.identity,
		Height = 0,
		Radius = 0
	} :: Cylinder

	function self.fromPart(basePart)
		self.Transform = basePart.CFrame
		self.Height = .5*basePart.Size.X
		self.Radius = .5*math.min(basePart.Size.Y, basePart.Size.Z)

		return self
	end

	@native function self.localSupport(n)
		local S = n.Y*n.Y + n.Z*n.Z
		if S == 0 then
			return Vector3.new(n.X < 0 and -self.Height or self.Height, self.Radius, 0)
		end

		local D = self.Radius/S^.5
		return Vector3.new(n.X < 0 and -self.Height or self.Height, n.Y*D, n.Z*D)
	end

	return self
end


--!strict

type Wedge = {
	Shape: string,
	Margin: number,

	Transform: CFrame,
	Extent: Vector3,

	fromPart: (basePart: BasePart) -> Wedge,
	localSupport: (n: Vector3) -> Vector3
}

local _vertices: {Vector3} = {
	Vector3.new( 1,  1,  1), Vector3.new(-1,  1,  1),
	Vector3.new(-1, -1,  1), Vector3.new( 1, -1, -1),
	Vector3.new(-1, -1, -1), Vector3.new( 1, -1,  1)
}

local _verticesCount: number = #_vertices

return function(): Wedge
	local self: Wedge = {
		Shape = 'Wedge',
		Margin = 0,

		Transform = CFrame.identity,
		Extent = Vector3.zero
	} :: Wedge

	function self.fromPart(basePart)
		self.Transform = basePart.CFrame
		self.Extent = .5*basePart.Size

		return self
	end

	@native function self.localSupport(n)
		local curP, bestD = _vertices[1], n:Dot(_vertices[1]) do
			for I = 2, _verticesCount do
				local d = n:Dot(_vertices[I])
				if d > bestD then
					curP, bestD = _vertices[I], d
				end
			end
		end

		return curP*self.Extent
	end

	return self
end


local ProcessService = game:GetService("ProcessService")
local Packages = script.Parent.PicomatchTestModel.Packages

local JestGlobals = require(Packages.Dev.JestGlobals)

local TestEZ = JestGlobals.TestEZ

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

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


-- ROBLOX NOTE: no upstream

-- ROBLOX TODO: implement in LuauPolyfill

local CurrentModule = script.Parent
local Packages = CurrentModule.Parent
local LuauPolyfill = require(Packages.LuauPolyfill)
local Array = LuauPolyfill.Array
type Array<T> = LuauPolyfill.Array<T>

local RegExp = require(Packages.RegExp)
type RegExp = RegExp.RegExp

local function stringReplace(str: string, regExp: RegExp, replFn: (...any) -> string)
	local v = str
	local match = regExp:exec(v)
	local offset = 0
	local replaceArr = {}
	while match ~= nil and match.index ~= nil do
		-- ROBLOX FIXME Luau: analyze complains about match type being `Array<string> & {| index: number?, input: string?, n: number |}` instead of table
		local m = (match :: Array<string>)[1]
		local args: Array<string | number> = Array.slice(match, 1, match.n + 1)
		-- ROBLOX FIXME Luau: analyze doesn't recognize match.index as a number
		local index = (match.index :: any) + offset

		table.insert(args, index)

		local replace = replFn(table.unpack(args))

		table.insert(replaceArr, {
			from = index,
			length = #m,
			value = replace,
		})

		-- ROBLOX FIXME Luau: analyze doesn't recognize match.index as a number
		offset += #m + (match.index :: any) - 1
		v = str:sub(offset + 1)
		match = regExp:exec(v)
	end
	local result = str:sub(1)
	for _, rep in ipairs(Array.reverse(replaceArr)) do
		local from, length, value = rep.from, rep.length, rep.value
		local prefix = result:sub(1, from - 1)
		local suffix = result:sub(from + length)

		result = prefix .. value .. suffix
	end

	return result
end

return {
	stringReplace = stringReplace,
}


local base64 = require("base64")

local llz4 = require("llz4")
local lz4 = require("lz4")
local llzw = require("llzw")

local data do
	local file = assert(io.open(assert(arg[1], "missing file name"), "r"))
	data = file:read("*a")
	file:close()
end

local function bench(which, compress, decompress, data)
	collectgarbage()
	collectgarbage()

	local compressStart = os.clock()
	local compressed, a, b, c, d = compress(data)
	local compressEnd = os.clock()

	collectgarbage()
	collectgarbage()

	local decompressStart = os.clock()
	local decompressed = decompress(compressed, a, b, c, d)
	local decompressEnd = os.clock()

	assert(decompressed == data, "decompressed doesn't match original")

	print(string.format(
		"%s:\n  Compress: %.2f s\n  Decompress: %.2f s\n  Ratio: %.2f (%d KiB -> %d KiB)",
		which,
		compressEnd - compressStart,
		decompressEnd - decompressStart,
		#data / #compressed, math.floor(#data / 1024), math.floor(#compressed / 1024)
	))
end

local string_byte = string.byte
local function justIter(s)
	for i = 1, #s do
		local c = string_byte(s, i)
	end
	return s
end

local function compose2(f, g)
	return function(...)
		return f(g(...))
	end
end

bench("iter", justIter, justIter, data)
bench("llz4", llz4.compress, llz4.decompress, data)
bench("llz4 + base64", compose2(base64.encode, llz4.compress), compose2(llz4.decompress, base64.decode), data)
bench("lz4" , lz4.compress, lz4.decompress, data)
bench("lz4 + base64" , compose2(base64.encode, lz4.compress), compose2(lz4.decompress, base64.decode), data)
bench("llzw", llzw.compress, llzw.decompress, data)


local base64 = require("../Base64")

local llz4 = require("../llz4-luau")
local llzw = require("../llzw-luau")

local data = require("../luau-data")[1]

local function bench(which, compress, decompress, data)
	collectgarbage()
	collectgarbage()

	local compressStart = os.clock()
	local compressed, a, b, c, d = compress(data)
	local compressEnd = os.clock()

	collectgarbage()
	collectgarbage()

	local decompressStart = os.clock()
	local decompressed = decompress(compressed, a, b, c, d)
	local decompressEnd = os.clock()

	assert(decompressed == data, "decompressed doesn't match original")

	print(string.format(
		"%s:\n  Compress: %.2f s\n  Decompress: %.2f s\n  Ratio: %.2f (%d KiB -> %d KiB)",
		which,
		compressEnd - compressStart,
		decompressEnd - decompressStart,
		#data / #compressed, math.floor(#data / 1024), math.floor(#compressed / 1024)
	))
end

local string_byte = string.byte
local function justIter(s)
	for i = 1, #s do
		local c = string_byte(s, i)
	end
	return s
end

bench("iter", justIter, justIter, data)
bench("llz4", llz4.compress, llz4.decompress, data)
bench("llz4 + base64", function(str)
	local buf = buffer.fromstring(str)
	local out, outLen = llz4.compressBuffer(buf, 0, #str)
	local toSize = buffer.create(outLen)
	buffer.copy(toSize, 0, out, 0, outLen)
	return buffer.tostring(base64.encode(toSize))
end, function(str)
	local buf = base64.decode(buffer.fromstring(str))
	local out, outLen = llz4.decompressBuffer(buf, 0, buffer.len(buf))
	return buffer.readstring(out, 0, outLen)
end, data)
bench("llzw", llzw.compress, llzw.decompress, data)


-- Turn an arbitrary file into a Lua file that returns a table with a string
-- holding the contents of said file, so that it can be loaded by Luau.
-- usage: lua requireify.lua <file>

local filename = assert(arg[1], "missing filename")

local file = assert(io.open(filename, "rb"))
local data = file:read("*a")
file:close()

file = assert(io.open("luau-data.luau", "wb"))
file:write(string.format([[
return {%q}
]], data))
file:close()


local llz4 = require("../llz4-test")
local str = (...)

for acceleration = 1, 10 do
	local compressed = llz4.compress(str, acceleration)
	local decompressed = llz4.decompress(compressed)
	assert(decompressed == str, "decompressed doesn't match original")
end