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

local Lexer = require("lexer")
local TOKEN_TYPE = Lexer.TOKEN_TYPE

type Token = Lexer.Token
type Span = Lexer.Span

local NODE_TYPE = {
	PROGRAM = "Program",
	FUNCTION_DECL = "FunctionDecl",
	LOCAL_DECL = "LocalDecl",
	ASSIGNMENT = "Assignment",
	IF_STMT = "IfStmt",
	WHILE_STMT = "WhileStmt",
	FOR_STMT = "ForStmt",
	REPEAT_STMT = "RepeatStmt",
	RETURN_STMT = "ReturnStmt",
	BREAK_STMT = "BreakStmt",
	CONTINUE_STMT = "ContinueStmt",
	EXPR_STMT = "ExprStmt",
	BLOCK = "Block",
	IDENTIFIER = "Identifier",
	NUMBER_LITERAL = "NumberLiteral",
	STRING_LITERAL = "StringLiteral",
	BOOL_LITERAL = "BoolLiteral",
	NIL_LITERAL = "NilLiteral",
	FUNCTION_CALL = "FunctionCall",
	BINARY_EXPR = "BinaryExpr",
	UNARY_EXPR = "UnaryExpr",
	TABLE_CONSTRUCTOR = "TableConstructor",
	TABLE_FIELD = "TableField",
	INDEX_EXPR = "IndexExpr",
	MEMBER_EXPR = "MemberExpr",
	TYPE_ANNOTATION = "TypeAnnotation",
	FUNCTION_TYPE = "FunctionType",
	TABLE_TYPE = "TableType",
	UNION_TYPE = "UnionType",
	INTERSECTION_TYPE = "IntersectionType",
	OPTIONAL_TYPE = "OptionalType",
	GENERIC_TYPE = "GenericType",
}

export type NodeType = string

export type ASTNode = {
	node_type: NodeType,
	span: Span,
	value: string?,
	children: {ASTNode}?,
	[string]: any,
}

type ParserState = {
	tokens: {Token},
	current: number,
	errors: {string},
	panic_mode: boolean,
	in_vararg_function: boolean,
}

local function create_node(node_type: NodeType, span: Span, value: string?): ASTNode
	return {
		node_type = node_type,
		span = span,
		value = value,
		children = {},
	}
end

local function merge_spans(start: Span, end_span: Span): Span
	return {
		start_line = start.start_line,
		start_column = start.start_column,
		end_line = end_span.end_line,
		end_column = end_span.end_column,
	}
end

local function format_node(node: ASTNode, indent: number): string
	local result = string.rep("  ", indent)
	local span_str = string.format("[%d:%d->%d:%d]",
		node.span.start_line, node.span.start_column,
		node.span.end_line, node.span.end_column)
	result ..= span_str .. " " .. node.node_type
	if node.value then
		if type(node.value) == "string" then
			result ..= ": \"" .. node.value .. "\""
		else
			result ..= ": " .. tostring(node.value)
		end
	end

	result ..= "\n"

	if node.children and #node.children > 0 then
		for _, child in node.children do
			result ..= format_node(child, indent + 1)
		end
	end

	if node.body and #node.body > 0 then
		for _, child in node.body do
			result ..= format_node(child, indent + 1)
		end
	end

	if node.names and #node.names > 0 then
		for _, name in node.names do
			result ..= format_node(name, indent + 1)
		end
	end

	if node.targets and #node.targets > 0 then
		for _, target in node.targets do
			result ..= format_node(target, indent + 1)
		end
	end

	if node.values and #node.values > 0 then
		for _, val in node.values do
			result ..= format_node(val, indent + 1)
		end
	end

	if node.params and #node.params > 0 then
		for _, param in node.params do
			result ..= format_node(param, indent + 1)
		end
	end

	if node.fields and #node.fields > 0 then
		for _, field in node.fields do
			result ..= format_node(field, indent + 1)
		end
	end

	if node.condition then
		result ..= format_node(node.condition, indent + 1)
	end

	if node.then_body and #node.then_body > 0 then
		for _, stmt in node.then_body do
			result ..= format_node(stmt, indent + 1)
		end
	end

	if node.else_body and #node.else_body > 0 then
		for _, stmt in node.else_body do
			result ..= format_node(stmt, indent + 1)
		end
	end

	if node.expression then
		result ..= format_node(node.expression, indent + 1)
	end

	if node.left then
		result ..= format_node(node.left, indent + 1)
	end

	if node.right then
		result ..= format_node(node.right, indent + 1)
	end

	if node.operand then
		result ..= format_node(node.operand, indent + 1)
	end

	if node.callee then
		result ..= format_node(node.callee, indent + 1)
	end

	if node.arguments and #node.arguments > 0 then
		for _, arg in node.arguments do
			result ..= format_node(arg, indent + 1)
		end
	end

	if node.object then
		result ..= format_node(node.object, indent + 1)
	end

	if node.property then
		result ..= format_node(node.property, indent + 1)
	end

	if node.index then
		result ..= format_node(node.index, indent + 1)
	end

	if node.key and node.node_type ~= NODE_TYPE.TABLE_FIELD then
		result ..= format_node(node.key, indent + 1)
	end

	if node.node_type == NODE_TYPE.TABLE_FIELD then
		if node.field_key then
			result ..= format_node(node.field_key, indent + 1)
		end
		if node.field_value then
			result ..= format_node(node.field_value, indent + 1)
		end
	end

	if node.elseif_branches and #node.elseif_branches > 0 then
		for _, branch in node.elseif_branches do
			if branch.condition then
				result ..= format_node(branch.condition, indent + 1)
			end
			if branch.body and #branch.body > 0 then
				for _, stmt in branch.body do
					result ..= format_node(stmt, indent + 1)
				end
			end
		end
	end

	if node.variable then
		result ..= format_node(node.variable, indent + 1)
	end

	if node.start then
		result ..= format_node(node.start, indent + 1)
	end

	if node.stop then
		result ..= format_node(node.stop, indent + 1)
	end

	if node.step then
		result ..= format_node(node.step, indent + 1)
	end

	if node.variables and #node.variables > 0 then
		for _, var in node.variables do
			result ..= format_node(var, indent + 1)
		end
	end

	if node.iterators and #node.iterators > 0 then
		for _, iter in node.iterators do
			result ..= format_node(iter, indent + 1)
		end
	end

	if node.name then
		result ..= format_node(node.name, indent + 1)
	end

	return result
end

local function peek(state: ParserState, offset: number?): Token
	local idx = state.current + (offset or 0)
	if idx >= #state.tokens then
		return state.tokens[#state.tokens]
	end
	return state.tokens[idx]
end

local function advance(state: ParserState): Token
	if state.current < #state.tokens then
		state.current += 1
	end
	return state.tokens[state.current - 1]
end

local function check(state: ParserState, type: number): boolean
	return peek(state).type == type
end

local function match(state: ParserState, ...: number): boolean
	for _, type in {...} do
		if check(state, type) then
			advance(state)
			return true
		end
	end
	return false
end

local function consume(state: ParserState, type: number, message: string): Token?
	if check(state, type) then
		return advance(state)
	end
	local token = peek(state)
	local error_msg = string.format("[%d:%d] Parse Error: %s",
		token.span.start_line, token.span.start_column, message)
	table.insert(state.errors, error_msg)
	state.panic_mode = true
	return nil
end

local function synchronize(state: ParserState)
	state.panic_mode = false
	advance(state)
	while not check(state, TOKEN_TYPE.EOF) do
		if peek(state, -1).type == TOKEN_TYPE.SEMICOLON then
			return
		end
		if match(state, TOKEN_TYPE.LOCAL, TOKEN_TYPE.FUNCTION, TOKEN_TYPE.IF,
			TOKEN_TYPE.WHILE, TOKEN_TYPE.FOR, TOKEN_TYPE.RETURN) then
			state.current -= 1
			return
		end
		advance(state)
	end
end

local parse_expression: (ParserState) -> ASTNode?
local parse_type_annotation: (ParserState) -> ASTNode?
local parse_statement: (ParserState) -> ASTNode?

local function parse_primary_expression(state: ParserState): ASTNode?
	if state.panic_mode then
		return nil
	end
	local token = peek(state)
	if match(state, TOKEN_TYPE.NUMBER) then
		return create_node(NODE_TYPE.NUMBER_LITERAL, token.span, token.lexeme)
	elseif match(state, TOKEN_TYPE.STRING, TOKEN_TYPE.INTERP_STRING) then
		return create_node(NODE_TYPE.STRING_LITERAL, token.span, token.lexeme)
	elseif match(state, TOKEN_TYPE.TRUE, TOKEN_TYPE.FALSE) then
		return create_node(NODE_TYPE.BOOL_LITERAL, token.span, token.lexeme)
	elseif match(state, TOKEN_TYPE.NIL) then
		return create_node(NODE_TYPE.NIL_LITERAL, token.span, "nil")
	elseif match(state, TOKEN_TYPE.DOT_DOT_DOT) then
		if not state.in_vararg_function then
			local error_msg = string.format("[%d:%d] Parse Error: Cannot use '...' outside of a vararg function",
				token.span.start_line, token.span.start_column)
			table.insert(state.errors, error_msg)
			state.panic_mode = true
			return nil
		end
		return create_node(NODE_TYPE.IDENTIFIER, token.span, "...")
	elseif match(state, TOKEN_TYPE.FUNCTION) then
		local start_token = peek(state, -1)
		consume(state, TOKEN_TYPE.LEFT_PAREN, "Expected '('")
		local params: {ASTNode} = {}
		local has_varargs = false
		if not check(state, TOKEN_TYPE.RIGHT_PAREN) then
			repeat
				if match(state, TOKEN_TYPE.DOT_DOT_DOT) then
					local vararg_token = peek(state, -1)
					local vararg_node = create_node(NODE_TYPE.IDENTIFIER, vararg_token.span, "...")
					table.insert(params, vararg_node)
					has_varargs = true
					break
				else
					local param_token = consume(state, TOKEN_TYPE.IDENTIFIER, "Expected parameter name")
					if param_token then
						local param_node = create_node(NODE_TYPE.IDENTIFIER, param_token.span, param_token.lexeme)
						if match(state, TOKEN_TYPE.COLON) then
							param_node.type_annotation = parse_type_annotation(state)
						end
						table.insert(params, param_node)
					end
				end
			until not match(state, TOKEN_TYPE.COMMA)
		end
		consume(state, TOKEN_TYPE.RIGHT_PAREN, "Expected ')'")
		local return_type: ASTNode? = nil
		if match(state, TOKEN_TYPE.COLON) then
			return_type = parse_type_annotation(state)
		end

		local prev_in_vararg = state.in_vararg_function
		state.in_vararg_function = has_varargs

		local body: {ASTNode} = {}
		while not check(state, TOKEN_TYPE.END) and not check(state, TOKEN_TYPE.EOF) do
			local stmt = parse_statement(state)
			if stmt then
				table.insert(body, stmt)
			end
		end

		state.in_vararg_function = prev_in_vararg
		local end_token = consume(state, TOKEN_TYPE.END, "Expected 'end'")
		if not end_token then
			return nil
		end
		local node = create_node(NODE_TYPE.FUNCTION_DECL,
			merge_spans(start_token.span, end_token.span), nil)
		node.params = params
		node.return_type = return_type
		node.body = body
		return node
	elseif match(state, TOKEN_TYPE.IF) then
		local start_token = peek(state, -1)
		local condition = parse_expression(state)
		if not condition then
			return nil
		end
		consume(state, TOKEN_TYPE.THEN, "Expected 'then'")
		local then_expr = parse_expression(state)
		if not then_expr then
			return nil
		end
		local else_expr: ASTNode? = nil
		if match(state, TOKEN_TYPE.ELSEIF) then
			state.current -= 1
			else_expr = parse_primary_expression(state)
		elseif match(state, TOKEN_TYPE.ELSE) then
			else_expr = parse_expression(state)
		end
		if not else_expr then
			return nil
		end
		local node = create_node(NODE_TYPE.IF_STMT,
			merge_spans(start_token.span, else_expr.span), "expr")
		node.condition = condition
		node.then_body = {then_expr}
		node.else_body = {else_expr}
		return node
	elseif match(state, TOKEN_TYPE.TYPEOF) then
		local start_token = peek(state, -1)
		consume(state, TOKEN_TYPE.LEFT_PAREN, "Expected '('")
		local expr = parse_expression(state)
		consume(state, TOKEN_TYPE.RIGHT_PAREN, "Expected ')'")
		if not expr then
			return nil
		end
		local node = create_node(NODE_TYPE.FUNCTION_CALL,
			merge_spans(start_token.span, expr.span), nil)
		node.callee = create_node(NODE_TYPE.IDENTIFIER, start_token.span, "typeof")
		node.arguments = {expr}
		return node
	elseif match(state, TOKEN_TYPE.IDENTIFIER) then
		local node = create_node(NODE_TYPE.IDENTIFIER, token.span, token.lexeme)
		while true do
			if match(state, TOKEN_TYPE.DOT) then
				local member_token = consume(state, TOKEN_TYPE.IDENTIFIER, "Expected identifier after '.'")
				if not member_token then
					return nil
				end
				local member_node = create_node(NODE_TYPE.MEMBER_EXPR,
					merge_spans(node.span, member_token.span), nil)
				member_node.object = node
				member_node.property = create_node(NODE_TYPE.IDENTIFIER, member_token.span, member_token.lexeme)
				node = member_node
			elseif match(state, TOKEN_TYPE.LEFT_BRACKET) then
				local index_expr = parse_expression(state)
				if not index_expr then
					return nil
				end
				local close_token = consume(state, TOKEN_TYPE.RIGHT_BRACKET, "Expected ']'")
				if not close_token then
					return nil
				end
				local index_node = create_node(NODE_TYPE.INDEX_EXPR,
					merge_spans(node.span, close_token.span), nil)
				index_node.object = node
				index_node.index = index_expr
				node = index_node
			elseif match(state, TOKEN_TYPE.COLON) then
				local method_token = consume(state, TOKEN_TYPE.IDENTIFIER, "Expected method name after ':'")
				if not method_token then
					return nil
				end
				consume(state, TOKEN_TYPE.LEFT_PAREN, "Expected '('")
				local args: {ASTNode} = {node}
				if not check(state, TOKEN_TYPE.RIGHT_PAREN) then
					repeat
						local arg = parse_expression(state)
						if arg then
							table.insert(args, arg)
						end
					until not match(state, TOKEN_TYPE.COMMA)
				end
				local close_token = consume(state, TOKEN_TYPE.RIGHT_PAREN, "Expected ')'")
				if not close_token then
					return nil
				end
				local method_access = create_node(NODE_TYPE.MEMBER_EXPR,
					merge_spans(node.span, method_token.span), nil)
				method_access.object = node
				method_access.property = create_node(NODE_TYPE.IDENTIFIER, method_token.span, method_token.lexeme)
				local call_node = create_node(NODE_TYPE.FUNCTION_CALL,
					merge_spans(node.span, close_token.span), nil)
				call_node.callee = method_access
				call_node.arguments = args
				node = call_node
			elseif match(state, TOKEN_TYPE.LEFT_PAREN) then
				local args: {ASTNode} = {}
				if not check(state, TOKEN_TYPE.RIGHT_PAREN) then
					repeat
						local arg = parse_expression(state)
						if arg then
							table.insert(args, arg)
						end
					until not match(state, TOKEN_TYPE.COMMA)
				end
				local close_token = consume(state, TOKEN_TYPE.RIGHT_PAREN, "Expected ')'")
				if not close_token then
					return nil
				end
				local call_node = create_node(NODE_TYPE.FUNCTION_CALL,
					merge_spans(node.span, close_token.span), nil)
				call_node.callee = node
				call_node.arguments = args
				node = call_node
			else
				break
			end
		end
		return node
	elseif match(state, TOKEN_TYPE.LEFT_PAREN) then
		local expr = parse_expression(state)
		consume(state, TOKEN_TYPE.RIGHT_PAREN, "Expected ')'")
		if not expr then
			return nil
		end
		local node = expr
		while true do
			if match(state, TOKEN_TYPE.LEFT_PAREN) then
				local args: {ASTNode} = {}
				if not check(state, TOKEN_TYPE.RIGHT_PAREN) then
					repeat
						local arg = parse_expression(state)
						if arg then
							table.insert(args, arg)
						end
					until not match(state, TOKEN_TYPE.COMMA)
				end
				local close_token = consume(state, TOKEN_TYPE.RIGHT_PAREN, "Expected ')'")
				if not close_token then
					return nil
				end
				local call_node = create_node(NODE_TYPE.FUNCTION_CALL,
					merge_spans(node.span, close_token.span), nil)
				call_node.callee = node
				call_node.arguments = args
				node = call_node
			elseif match(state, TOKEN_TYPE.LEFT_BRACKET) then
				local index_expr = parse_expression(state)
				if not index_expr then
					return nil
				end
				local close_token = consume(state, TOKEN_TYPE.RIGHT_BRACKET, "Expected ']'")
				if not close_token then
					return nil
				end
				local index_node = create_node(NODE_TYPE.INDEX_EXPR,
					merge_spans(node.span, close_token.span), nil)
				index_node.object = node
				index_node.index = index_expr
				node = index_node
			elseif match(state, TOKEN_TYPE.DOT) then
				local member_token = consume(state, TOKEN_TYPE.IDENTIFIER, "Expected identifier after '.'")
				if not member_token then
					return nil
				end
				local member_node = create_node(NODE_TYPE.MEMBER_EXPR,
					merge_spans(node.span, member_token.span), nil)
				member_node.object = node
				member_node.property = create_node(NODE_TYPE.IDENTIFIER, member_token.span, member_token.lexeme)
				node = member_node
			else
				break
			end
		end
		return node
	elseif match(state, TOKEN_TYPE.LEFT_BRACE) then
		local start_span = peek(state, -1).span
		local fields: {ASTNode} = {}
		if not check(state, TOKEN_TYPE.RIGHT_BRACE) then
			repeat
				if match(state, TOKEN_TYPE.LEFT_BRACKET) then
					local key = parse_expression(state)
					consume(state, TOKEN_TYPE.RIGHT_BRACKET, "Expected ']'")
					consume(state, TOKEN_TYPE.EQUAL, "Expected '='")
					local val = parse_expression(state)
					if key and val then
						local field = create_node(NODE_TYPE.TABLE_FIELD,
							merge_spans(key.span, val.span), nil)
						field.field_key = key
						field.field_value = val
						table.insert(fields, field)
					end
				elseif peek(state).type == TOKEN_TYPE.IDENTIFIER and peek(state, 1).type == TOKEN_TYPE.EQUAL then
					local key_token = advance(state)
					advance(state)
					local val = parse_expression(state)
					if val then
						local field = create_node(NODE_TYPE.TABLE_FIELD,
							merge_spans(key_token.span, val.span), nil)
						field.field_key = create_node(NODE_TYPE.IDENTIFIER, key_token.span, key_token.lexeme)
						field.field_value = val
						table.insert(fields, field)
					end
				elseif match(state, TOKEN_TYPE.DOT_DOT_DOT) then
					local vararg_token = peek(state, -1)
					local vararg_node = create_node(NODE_TYPE.IDENTIFIER, vararg_token.span, "...")
					table.insert(fields, vararg_node)
				else
					local val = parse_expression(state)
					if val then
						table.insert(fields, val)
					end
				end
			until not match(state, TOKEN_TYPE.COMMA)
		end
		local close_token = consume(state, TOKEN_TYPE.RIGHT_BRACE, "Expected '}'")
		if not close_token then
			return nil
		end
		local node = create_node(NODE_TYPE.TABLE_CONSTRUCTOR,
			merge_spans(start_span, close_token.span), nil)
		node.fields = fields
		return node
	end
	local error_token = peek(state)
	local error_msg = string.format("[%d:%d] Parse Error: Unexpected token '%s'",
		error_token.span.start_line, error_token.span.start_column, error_token.lexeme)
	table.insert(state.errors, error_msg)
	state.panic_mode = true
	return nil
end

local function parse_unary_expression(state: ParserState): ASTNode?
	if match(state, TOKEN_TYPE.NOT, TOKEN_TYPE.MINUS, TOKEN_TYPE.HASH) then
		local op_token = peek(state, -1)
		local operand = parse_unary_expression(state)
		if not operand then
			return nil
		end
		local node = create_node(NODE_TYPE.UNARY_EXPR,
			merge_spans(op_token.span, operand.span), op_token.lexeme)
		node.operand = operand
		return node
	end
	return parse_primary_expression(state)
end

local function parse_binary_expression(state: ParserState, min_precedence: number): ASTNode?
	local left = parse_unary_expression(state)
	if not left then
		return nil
	end
	local precedence_table = {
		[TOKEN_TYPE.OR] = 1,
		[TOKEN_TYPE.AND] = 2,
		[TOKEN_TYPE.LESS] = 3,
		[TOKEN_TYPE.GREATER] = 3,
		[TOKEN_TYPE.LESS_EQUAL] = 3,
		[TOKEN_TYPE.GREATER_EQUAL] = 3,
		[TOKEN_TYPE.EQUAL_EQUAL] = 3,
		[TOKEN_TYPE.TILDE_EQUAL] = 3,
		[TOKEN_TYPE.DOT_DOT] = 4,
		[TOKEN_TYPE.PLUS] = 5,
		[TOKEN_TYPE.MINUS] = 5,
		[TOKEN_TYPE.STAR] = 6,
		[TOKEN_TYPE.SLASH] = 6,
		[TOKEN_TYPE.PERCENT] = 6,
		[TOKEN_TYPE.CARET] = 7,
	}
	while true do
		local op_type = peek(state).type
		local precedence = precedence_table[op_type]
		if not precedence or precedence < min_precedence then
			break
		end
		local op_token = advance(state)
		local right = parse_binary_expression(state, precedence + 1)
		if not right then
			return nil
		end
		local node = create_node(NODE_TYPE.BINARY_EXPR,
			merge_spans(left.span, right.span), op_token.lexeme)
		node.left = left
		node.right = right
		left = node
	end
	return left
end

function parse_expression(state: ParserState): ASTNode?
	return parse_binary_expression(state, 1)
end

function parse_type_annotation(state: ParserState): ASTNode?
	if state.panic_mode then
		return nil
	end
	local start_token = peek(state)
	if match(state, TOKEN_TYPE.IDENTIFIER) then
		local node = create_node(NODE_TYPE.TYPE_ANNOTATION, start_token.span, start_token.lexeme)
		if match(state, TOKEN_TYPE.LESS) then
			local generics: {ASTNode} = {}
			repeat
				local generic = parse_type_annotation(state)
				if generic then
					table.insert(generics, generic)
				end
			until not match(state, TOKEN_TYPE.COMMA)
			consume(state, TOKEN_TYPE.GREATER, "Expected '>'")
			node.generics = generics
		end
		return node
	elseif match(state, TOKEN_TYPE.LEFT_PAREN) then
		local params: {ASTNode} = {}
		if not check(state, TOKEN_TYPE.RIGHT_PAREN) then
			repeat
				local param = parse_type_annotation(state)
				if param then
					table.insert(params, param)
				end
			until not match(state, TOKEN_TYPE.COMMA)
		end
		consume(state, TOKEN_TYPE.RIGHT_PAREN, "Expected ')'")
		consume(state, TOKEN_TYPE.ARROW, "Expected '->'")
		local return_type = parse_type_annotation(state)
		local node = create_node(NODE_TYPE.FUNCTION_TYPE,
			merge_spans(start_token.span, (return_type and return_type.span or start_token.span)), nil)
		node.params = params
		node.return_type = return_type
		return node
	elseif match(state, TOKEN_TYPE.LEFT_BRACE) then
		local fields: {ASTNode} = {}
		if not check(state, TOKEN_TYPE.RIGHT_BRACE) then
			repeat
				local field_name = consume(state, TOKEN_TYPE.IDENTIFIER, "Expected field name")
				if not field_name then
					break
				end
				consume(state, TOKEN_TYPE.COLON, "Expected ':'")
				local field_type = parse_type_annotation(state)
				if field_type then
					local field_node = create_node(NODE_TYPE.TABLE_TYPE,
						merge_spans(field_name.span, field_type.span), field_name.lexeme)
					field_node.field_type = field_type
					table.insert(fields, field_node)
				end
			until not match(state, TOKEN_TYPE.COMMA)
		end
		local close_token = consume(state, TOKEN_TYPE.RIGHT_BRACE, "Expected '}'")
		if not close_token then
			return nil
		end
		local node = create_node(NODE_TYPE.TABLE_TYPE,
			merge_spans(start_token.span, close_token.span), nil)
		node.fields = fields
		return node
	end
	return nil
end

function parse_statement(state: ParserState): ASTNode?
	if state.panic_mode then
		synchronize(state)
		return nil
	end

	if match(state, TOKEN_TYPE.SEMICOLON) then
		return parse_statement(state)
	end

	if match(state, TOKEN_TYPE.LOCAL) then
		local start_token = peek(state, -1)
		if match(state, TOKEN_TYPE.FUNCTION) then
			local name_token = consume(state, TOKEN_TYPE.IDENTIFIER, "Expected function name")
			if not name_token then
				return nil
			end
			consume(state, TOKEN_TYPE.LEFT_PAREN, "Expected '('")
			local params: {ASTNode} = {}
			local has_varargs = false
			if not check(state, TOKEN_TYPE.RIGHT_PAREN) then
				repeat
					if match(state, TOKEN_TYPE.DOT_DOT_DOT) then
						local vararg_token = peek(state, -1)
						local vararg_node = create_node(NODE_TYPE.IDENTIFIER, vararg_token.span, "...")
						table.insert(params, vararg_node)
						has_varargs = true
						break
					else
						local param_token = consume(state, TOKEN_TYPE.IDENTIFIER, "Expected parameter name")
						if param_token then
							local param_node = create_node(NODE_TYPE.IDENTIFIER, param_token.span, param_token.lexeme)
							if match(state, TOKEN_TYPE.COLON) then
								param_node.type_annotation = parse_type_annotation(state)
							end
							table.insert(params, param_node)
						end
					end
				until not match(state, TOKEN_TYPE.COMMA)
			end
			consume(state, TOKEN_TYPE.RIGHT_PAREN, "Expected ')'")
			local return_type: ASTNode? = nil
			if match(state, TOKEN_TYPE.COLON) then
				return_type = parse_type_annotation(state)
			end

			local prev_in_vararg = state.in_vararg_function
			state.in_vararg_function = has_varargs

			local body: {ASTNode} = {}
			while not check(state, TOKEN_TYPE.END) and not check(state, TOKEN_TYPE.EOF) do
				local stmt = parse_statement(state)
				if stmt then
					table.insert(body, stmt)
				end
			end

			state.in_vararg_function = prev_in_vararg
			local end_token = consume(state, TOKEN_TYPE.END, "Expected 'end'")
			if not end_token then
				return nil
			end
			local node = create_node(NODE_TYPE.FUNCTION_DECL,
				merge_spans(start_token.span, end_token.span), name_token.lexeme)
			node.params = params
			node.return_type = return_type
			node.body = body
			node.is_local = true
			return node
		else
			local names: {ASTNode} = {}
			repeat
				local name_token = consume(state, TOKEN_TYPE.IDENTIFIER, "Expected variable name")
				if name_token then
					local name_node = create_node(NODE_TYPE.IDENTIFIER, name_token.span, name_token.lexeme)
					if match(state, TOKEN_TYPE.COLON) then
						name_node.type_annotation = parse_type_annotation(state)
					end
					table.insert(names, name_node)
				end
			until not match(state, TOKEN_TYPE.COMMA)
			local values: {ASTNode} = {}
			if match(state, TOKEN_TYPE.EQUAL) then
				repeat
					local value = parse_expression(state)
					if value then
						table.insert(values, value)
					end
				until not match(state, TOKEN_TYPE.COMMA)
			end
			match(state, TOKEN_TYPE.SEMICOLON)
			local node = create_node(NODE_TYPE.LOCAL_DECL,
				merge_spans(start_token.span, (values[#values] and values[#values].span or names[#names].span)), nil)
			node.names = names
			node.values = values
			return node
		end
	elseif match(state, TOKEN_TYPE.FUNCTION) then
		local start_token = peek(state, -1)
		local name_token = consume(state, TOKEN_TYPE.IDENTIFIER, "Expected function name")
		if not name_token then
			return nil
		end
		local function_name = name_token.lexeme
		local name_node = create_node(NODE_TYPE.IDENTIFIER, name_token.span, name_token.lexeme)
		while match(state, TOKEN_TYPE.DOT) or match(state, TOKEN_TYPE.COLON) do
			local is_method = peek(state, -1).type == TOKEN_TYPE.COLON
			local member_token = consume(state, TOKEN_TYPE.IDENTIFIER, "Expected method name")
			if not member_token then
				return nil
			end
			local member_node = create_node(NODE_TYPE.MEMBER_EXPR,
				merge_spans(name_node.span, member_token.span), nil)
			member_node.object = name_node
			member_node.property = create_node(NODE_TYPE.IDENTIFIER, member_token.span, member_token.lexeme)
			member_node.is_method = is_method
			name_node = member_node
		end

		consume(state, TOKEN_TYPE.LEFT_PAREN, "Expected '('")
		local params: {ASTNode} = {}
		local has_varargs = false
		if not check(state, TOKEN_TYPE.RIGHT_PAREN) then
			repeat
				if match(state, TOKEN_TYPE.DOT_DOT_DOT) then
					local vararg_token = peek(state, -1)
					local vararg_node = create_node(NODE_TYPE.IDENTIFIER, vararg_token.span, "...")
					table.insert(params, vararg_node)
					has_varargs = true
					break
				else
					local param_token = consume(state, TOKEN_TYPE.IDENTIFIER, "Expected parameter name")
					if param_token then
						local param_node = create_node(NODE_TYPE.IDENTIFIER, param_token.span, param_token.lexeme)
						if match(state, TOKEN_TYPE.COLON) then
							param_node.type_annotation = parse_type_annotation(state)
						end
						table.insert(params, param_node)
					end
				end
			until not match(state, TOKEN_TYPE.COMMA)
		end
		consume(state, TOKEN_TYPE.RIGHT_PAREN, "Expected ')'")
		local return_type: ASTNode? = nil
		if match(state, TOKEN_TYPE.COLON) then
			return_type = parse_type_annotation(state)
		end

		local prev_in_vararg = state.in_vararg_function
		state.in_vararg_function = has_varargs

		local body: {ASTNode} = {}
		while not check(state, TOKEN_TYPE.END) and not check(state, TOKEN_TYPE.EOF) do
			local stmt = parse_statement(state)
			if stmt then
				table.insert(body, stmt)
			end
		end

		state.in_vararg_function = prev_in_vararg
		local end_token = consume(state, TOKEN_TYPE.END, "Expected 'end'")
		if not end_token then
			return nil
		end
		local node = create_node(NODE_TYPE.FUNCTION_DECL,
			merge_spans(start_token.span, end_token.span), function_name)
		node.name = name_node
		node.params = params
		node.return_type = return_type
		node.body = body
		node.is_local = false
		return node
	elseif match(state, TOKEN_TYPE.IF) then
		local start_token = peek(state, -1)
		local condition = parse_expression(state)
		if not condition then
			return nil
		end
		consume(state, TOKEN_TYPE.THEN, "Expected 'then'")

		local then_body: {ASTNode} = {}
		while not check(state, TOKEN_TYPE.ELSEIF) and not check(state, TOKEN_TYPE.ELSE)
			and not check(state, TOKEN_TYPE.END) and not check(state, TOKEN_TYPE.EOF) do
			local stmt = parse_statement(state)
			if stmt then
				table.insert(then_body, stmt)
			end
		end

		local elseif_branches: {{condition: ASTNode, body: {ASTNode}}} = {}
		while match(state, TOKEN_TYPE.ELSEIF) do
			local elseif_condition = parse_expression(state)
			if not elseif_condition then
				return nil
			end
			consume(state, TOKEN_TYPE.THEN, "Expected 'then'")
			local elseif_body: {ASTNode} = {}
			while not check(state, TOKEN_TYPE.ELSEIF) and not check(state, TOKEN_TYPE.ELSE)
				and not check(state, TOKEN_TYPE.END) and not check(state, TOKEN_TYPE.EOF) do
				local stmt = parse_statement(state)
				if stmt then
					table.insert(elseif_body, stmt)
				end
			end
			table.insert(elseif_branches, {condition = elseif_condition, body = elseif_body})
		end

		local else_body: {ASTNode}? = nil
		if match(state, TOKEN_TYPE.ELSE) then
			else_body = {}
			while not check(state, TOKEN_TYPE.END) and not check(state, TOKEN_TYPE.EOF) do
				local stmt = parse_statement(state)
				if stmt then
					table.insert(else_body, stmt)
				end
			end
		end

		local end_token = consume(state, TOKEN_TYPE.END, "Expected 'end'")
		if not end_token then
			return nil
		end
		match(state, TOKEN_TYPE.SEMICOLON)
		local node = create_node(NODE_TYPE.IF_STMT,
			merge_spans(start_token.span, end_token.span), nil)
		node.condition = condition
		node.then_body = then_body
		node.elseif_branches = elseif_branches
		node.else_body = else_body
		return node
	elseif match(state, TOKEN_TYPE.WHILE) then
		local start_token = peek(state, -1)
		local condition = parse_expression(state)
		if not condition then
			return nil
		end
		consume(state, TOKEN_TYPE.DO, "Expected 'do'")

		local body: {ASTNode} = {}
		while not check(state, TOKEN_TYPE.END) and not check(state, TOKEN_TYPE.EOF) do
			local stmt = parse_statement(state)
			if stmt then
				table.insert(body, stmt)
			end
		end

		local end_token = consume(state, TOKEN_TYPE.END, "Expected 'end'")
		if not end_token then
			return nil
		end
		match(state, TOKEN_TYPE.SEMICOLON)
		local node = create_node(NODE_TYPE.WHILE_STMT,
			merge_spans(start_token.span, end_token.span), nil)
		node.condition = condition
		node.body = body
		return node
	elseif match(state, TOKEN_TYPE.REPEAT) then
		local start_token = peek(state, -1)

		local body: {ASTNode} = {}
		while not check(state, TOKEN_TYPE.UNTIL) and not check(state, TOKEN_TYPE.EOF) do
			local stmt = parse_statement(state)
			if stmt then
				table.insert(body, stmt)
			end
		end

		local until_token = consume(state, TOKEN_TYPE.UNTIL, "Expected 'until'")
		if not until_token then
			return nil
		end

		local condition = parse_expression(state)
		if not condition then
			return nil
		end
		match(state, TOKEN_TYPE.SEMICOLON)
		local node = create_node(NODE_TYPE.REPEAT_STMT,
			merge_spans(start_token.span, condition.span), nil)
		node.body = body
		node.condition = condition
		return node
	elseif match(state, TOKEN_TYPE.FOR) then
		local start_token = peek(state, -1)
		local var_token = consume(state, TOKEN_TYPE.IDENTIFIER, "Expected variable name")
		if not var_token then
			return nil
		end

		if match(state, TOKEN_TYPE.EQUAL) then
			local start_expr = parse_expression(state)
			if not start_expr then
				return nil
			end
			consume(state, TOKEN_TYPE.COMMA, "Expected ','")
			local stop_expr = parse_expression(state)
			if not stop_expr then
				return nil
			end
			local step_expr: ASTNode? = nil
			if match(state, TOKEN_TYPE.COMMA) then
				step_expr = parse_expression(state)
			end
			consume(state, TOKEN_TYPE.DO, "Expected 'do'")

			local body: {ASTNode} = {}
			while not check(state, TOKEN_TYPE.END) and not check(state, TOKEN_TYPE.EOF) do
				local stmt = parse_statement(state)
				if stmt then
					table.insert(body, stmt)
				end
			end

			local end_token = consume(state, TOKEN_TYPE.END, "Expected 'end'")
			if not end_token then
				return nil
			end
			match(state, TOKEN_TYPE.SEMICOLON)
			local node = create_node(NODE_TYPE.FOR_STMT,
				merge_spans(start_token.span, end_token.span), "numeric")
			node.variable = create_node(NODE_TYPE.IDENTIFIER, var_token.span, var_token.lexeme)
			node.start = start_expr
			node.stop = stop_expr
			node.step = step_expr
			node.body = body
			return node
		else
			local variables: {ASTNode} = {create_node(NODE_TYPE.IDENTIFIER, var_token.span, var_token.lexeme)}
			while match(state, TOKEN_TYPE.COMMA) do
				local next_var = consume(state, TOKEN_TYPE.IDENTIFIER, "Expected variable name")
				if next_var then
					table.insert(variables, create_node(NODE_TYPE.IDENTIFIER, next_var.span, next_var.lexeme))
				end
			end
			consume(state, TOKEN_TYPE.IN, "Expected 'in'")

			local iterators: {ASTNode} = {}
			repeat
				local iter = parse_expression(state)
				if iter then
					table.insert(iterators, iter)
				end
			until not match(state, TOKEN_TYPE.COMMA)

			consume(state, TOKEN_TYPE.DO, "Expected 'do'")

			local body: {ASTNode} = {}
			while not check(state, TOKEN_TYPE.END) and not check(state, TOKEN_TYPE.EOF) do
				local stmt = parse_statement(state)
				if stmt then
					table.insert(body, stmt)
				end
			end

			local end_token = consume(state, TOKEN_TYPE.END, "Expected 'end'")
			if not end_token then
				return nil
			end
			match(state, TOKEN_TYPE.SEMICOLON)
			local node = create_node(NODE_TYPE.FOR_STMT,
				merge_spans(start_token.span, end_token.span), "generic")
			node.variables = variables
			node.iterators = iterators
			node.body = body
			return node
		end

	elseif match(state, TOKEN_TYPE.BREAK) then
		local token = peek(state, -1)
		match(state, TOKEN_TYPE.SEMICOLON)
		return create_node(NODE_TYPE.BREAK_STMT, token.span, nil)

	elseif match(state, TOKEN_TYPE.CONTINUE) then
		local token = peek(state, -1)
		match(state, TOKEN_TYPE.SEMICOLON)
		return create_node(NODE_TYPE.CONTINUE_STMT, token.span, nil)

	elseif match(state, TOKEN_TYPE.RETURN) then
		local start_token = peek(state, -1)
		local values: {ASTNode} = {}
		if not check(state, TOKEN_TYPE.END) and not check(state, TOKEN_TYPE.SEMICOLON) and not check(state, TOKEN_TYPE.EOF) then
			repeat
				local value = parse_expression(state)
				if value then
					table.insert(values, value)
				end
			until not match(state, TOKEN_TYPE.COMMA)
		end
		match(state, TOKEN_TYPE.SEMICOLON)
		local node = create_node(NODE_TYPE.RETURN_STMT,
			merge_spans(start_token.span, (values[#values] and values[#values].span or start_token.span)), nil)
		node.values = values
		return node

	else
		local expr = parse_expression(state)
		if not expr then
			return nil
		end

		if match(state, TOKEN_TYPE.EQUAL) or match(state, TOKEN_TYPE.COMMA) then
			local targets: {ASTNode} = {expr}

			if peek(state, -1).type == TOKEN_TYPE.COMMA then
				repeat
					local target = parse_expression(state)
					if target then
						table.insert(targets, target)
					end
				until not match(state, TOKEN_TYPE.COMMA)
				consume(state, TOKEN_TYPE.EQUAL, "Expected '='")
			end

			local values: {ASTNode} = {}
			repeat
				local value = parse_expression(state)
				if value then
					table.insert(values, value)
				end
			until not match(state, TOKEN_TYPE.COMMA)

			match(state, TOKEN_TYPE.SEMICOLON)
			local end_span = values[#values] and values[#values].span or targets[#targets].span
			local node = create_node(NODE_TYPE.ASSIGNMENT,
				merge_spans(targets[1].span, end_span), "=")
			node.targets = targets
			node.values = values
			return node
		else
			match(state, TOKEN_TYPE.SEMICOLON)
			local node = create_node(NODE_TYPE.EXPR_STMT, expr.span, nil)
			node.expression = expr
			return node
		end
	end
end

local function parse(tokens: {Token}): (ASTNode?, {string})
	local state: ParserState = {
		tokens = tokens,
		current = 1,
		errors = {},
		panic_mode = false,
		in_vararg_function = false,
	}
	local statements: {ASTNode} = {}
	while not check(state, TOKEN_TYPE.EOF) do
		local stmt = parse_statement(state)
		if stmt then
			table.insert(statements, stmt)
		end
		if state.panic_mode then
			synchronize(state)
		end
	end
	if #statements == 0 then
		return nil, state.errors
	end
	local program = create_node(NODE_TYPE.PROGRAM,
		merge_spans(statements[1].span, statements[#statements].span), nil)
	program.children = statements
	return program, state.errors
end

return {
	parse = parse,
	format_node = format_node,
	NODE_TYPE = NODE_TYPE,
}


local lx = require("lexer")
local prs = require("parser")

local src = [[
local x: typeof(
    (function(...)
        local tbl = {
            [((function() return "key" end)())] = {
                nested = (function(a, b, c)
                    return {
                        value = if a > b then
                            (function() return c ^ 2 end)()
                        else
                            (if b < c then #"test" else -99),
                        computed = ({
                            [1 + 2 * 3] = function(self)
                                return self.x or self.y
                            end,
                            data = `{tostring(math.sqrt(16))} items`
                        })[4],
                        chain = workspace:GetService("Players"):GetPlayers()[1].Name
                    }
                end)(1, 2, 3),
                array = {1, 2, (function() return {...} end)(4, 5, 6)}
            },
            [(true and "active" or "inactive")] = ((-#{1,2,3} * 2 + #"hello") ^ 2) % 10
        }
        return tbl[(function() return "key" end)()].nested.value
    end)(...)
) = 42
]]

local tokenised = lx.tokenize(src)
local parsed = prs.parse(tokenised)
print(tokenised)
print("-----------------------------")
print(parsed)
print("-----------------------------")


--!strict
--!optimize 2
-- Client-side bench receiver.

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local LyncFolder = ReplicatedStorage:WaitForChild("Lync")
local Lync = require(LyncFolder)
local benchFolder = LyncFolder:WaitForChild("bench")
local Scenarios = require(benchFolder:WaitForChild("Scenarios"))

Lync.start()

for _, packet in Scenarios.allPackets do
    packet:on(function(): () end)
end

-- Wait for server to finish CPU benchmarks before signaling ready
Scenarios.ServerReady:wait()
Scenarios.Ready:send(true)


--!strict
--!optimize 2
-- Shared type definitions for the Lync networking library.

export type ChannelState = {
    buff: buffer,
    cursor: number,
    size: number,
    refs: { Instance },
    refCount: number,
    lastId: number,
    countPos: number,
    itemCount: number,
    singleMode: boolean,
    singlePos: number,
    deltas: { [number]: any },
    prevDump: buffer?,
    prevDumpLen: number,
    currentPacket: string?,
}

export type Codec<T> = {
    write: (ch: ChannelState, value: T) -> (),
    read: (src: buffer, pos: number, refs: { Instance }?) -> (T, number),
}

export type InternalCodec<T> = {
    write: (ch: ChannelState, value: T) -> (),
    read: (src: buffer, pos: number, refs: { Instance }?) -> (T, number),
    _size: number?,
    _directWrite: ((b: buffer, offset: number, value: T) -> ())?,
    _directRead: ((b: buffer, offset: number) -> T)?,
    _isDelta: boolean?,
    _isBool: boolean?,
    _hasUnknown: boolean?,
    _min: number?,
    _max: number?,
    _isInteger: boolean?,
    _schema: { [string]: InternalCodec<any> }?,
    _typeCheck: string?,
}

export type RateLimitState = {
    tokens: number,
    lastRefill: number,
    lastAccepted: number,
}

export type RateLimitConfig = {
    maxPerSecond: number?,
    burst: number?,
    cooldown: number?,
}

export type PacketOptions = {
    unreliable: boolean?,
    rateLimit: RateLimitConfig?,
    validate: ((data: any, player: Player) -> (boolean, string?))?,
    maxPayloadBytes: number?,
    timestamp: string?,
}

export type QueryOptions = {
    timeout: number?,
    rateLimit: RateLimitConfig?,
    validate: ((data: any, player: Player) -> (boolean, string?))?,
}

export type Registration = {
    id: number,
    name: string,
    kind: number,
    codec: InternalCodec<any>,
    isUnreliable: boolean,
    partner: number?,
    signal: SignalLike,
    rateLimit: RateLimitConfig?,
    validate: ((data: any, player: Player) -> (boolean, string?))?,
    needsGate: boolean,
    maxPayloadBytes: number?,
    timestampMode: number,
    headerSize: number,
    _openFn: (ch: ChannelState, id: number) -> (),
    bytesSent: number,
    bytesReceived: number,
    fires: number,
    recvFires: number,
    drops: number,
}

export type SignalLike = {
    fire: (self: SignalLike, ...any) -> (),
    connect: (self: SignalLike, callback: (...any) -> ()) -> Connection,
}

export type Connection = {
    connected: boolean,
    disconnect: (self: Connection) -> (),
}

export type ConfigureOptions = {
    channelMaxSize: number?,
    validationDepth: number?,
    poolSize: number?,
    bandwidthLimit: {
        softLimit: number,
        maxStrikes: number,
    }?,
    globalRateLimit: {
        maxPerSecond: number,
    }?,
    stats: boolean?,
}

export type PacketStats = {
    bytesSent: number,
    bytesReceived: number,
    fires: number,
    recvFires: number,
    drops: number,
}

export type PlayerStats = {
    bytesSent: number,
    bytesReceived: number,
}

return nil


--!strict
--!optimize 2
-- Named player sets with __iter, auto-cleanup on PlayerRemoving.

local Players = game:GetService("Players")

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

-- State ---------------------------------------------------------------

local _groups: { [string]: any } = {}
local _playerCleanup: { [Player]: { any } } = {}
local _cleanupConnected = false

-- Private -------------------------------------------------------------

local function ensureCleanup(): ()
    if _cleanupConnected then
        return
    end
    _cleanupConnected = true

    Players.PlayerRemoving:Connect(function(player: Player)
        local groups = _playerCleanup[player]
        if not groups then
            return
        end

        for _, group in groups do
            group._members[player] = nil
            group._count -= 1
        end

        _playerCleanup[player] = nil
    end)
end

-- Public --------------------------------------------------------------

local Group = {}

export type GroupHandle = {
    add: (self: GroupHandle, player: Player) -> boolean,
    remove: (self: GroupHandle, player: Player) -> boolean,
    has: (self: GroupHandle, player: Player) -> boolean,
    count: (self: GroupHandle) -> number,
    destroy: (self: GroupHandle) -> (),
    _lyncGroup: boolean,
    _members: { [Player]: boolean },
}

local GroupMeta = {}
GroupMeta.__index = GroupMeta

function GroupMeta.__iter(self: any): () -> (Player?, boolean?)
    local members = self._members
    local key: Player? = nil

    return function(): (Player?, boolean?)
        local nextKey, nextVal = next(members, key)
        key = nextKey
        if nextKey then
            return nextKey, nextVal
        end
        return nil, nil
    end
end

function GroupMeta.add(self: any, player: Player): boolean
    if self._members[player] then
        return false
    end

    self._members[player] = true
    self._count += 1

    -- Track for auto-cleanup
    local cleanup = _playerCleanup[player]
    if not cleanup then
        cleanup = {}
        _playerCleanup[player] = cleanup
    end
    table.insert(cleanup, self)

    return true
end

function GroupMeta.remove(self: any, player: Player): boolean
    if not self._members[player] then
        return false
    end

    self._members[player] = nil
    self._count -= 1

    -- Remove from cleanup tracking
    local cleanup = _playerCleanup[player]
    if cleanup then
        local idx = table.find(cleanup, self)
        if idx then
            local last = #cleanup
            cleanup[idx] = cleanup[last]
            cleanup[last] = nil
        end
    end

    return true
end

function GroupMeta.has(self: any, player: Player): boolean
    return self._members[player] == true
end

function GroupMeta.count(self: any): number
    return self._count
end

function GroupMeta.destroy(self: any): ()
    local name = self._name
    if name then
        _groups[name] = nil
    end

    -- Remove all members from cleanup tracking
    for player in self._members do
        local cleanup = _playerCleanup[player]
        if cleanup then
            local idx = table.find(cleanup, self)
            if idx then
                local last = #cleanup
                cleanup[idx] = cleanup[last]
                cleanup[last] = nil
            end
        end
    end

    table.clear(self._members)
    self._count = 0
end

function Group.create(name: string): GroupHandle
    if _groups[name] then
        error(`[Lync] Lync.group: "{name}" already exists`)
    end

    ensureCleanup()

    local group = setmetatable({
        _lyncGroup = true,
        _members = {} :: { [Player]: boolean },
        _count = 0,
        _name = name,
    }, GroupMeta)

    _groups[name] = group
    return group :: any
end

return table.freeze(Group)


--!strict
--!optimize 2
-- Packet definition, send, on, once, wait, stats.

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

local Baseline = require(script.Parent.Parent.internal.Baseline)
local Channel = require(script.Parent.Parent.internal.Channel)
local Middleware = require(script.Parent.Parent.internal.Middleware)
local Registry = require(script.Parent.Parent.internal.Registry)
local Types = require(script.Parent.Parent.Types)

-- Constants -----------------------------------------------------------

local IS_SERVER = RunService:IsServer()

-- Private -------------------------------------------------------------

local function resolveTimestampMode(ts: string?): (number, number)
    if ts == "frame" then
        Channel.enableTimestamps()
        return Channel.TS_FRAME, 2
    elseif ts == "offset" then
        Channel.enableTimestamps()
        return Channel.TS_OFFSET, 3
    elseif ts == "full" then
        Channel.enableTimestamps()
        return Channel.TS_FULL, 9
    end
    return Channel.TS_NONE, 1
end

-- Public --------------------------------------------------------------

local Packet = {}

export type PacketHandle = {
    send: (self: PacketHandle, data: any, target: any?) -> (),
    on: (self: PacketHandle, fn: (...any) -> ()) -> Types.Connection,
    once: (self: PacketHandle, fn: (...any) -> ()) -> Types.Connection,
    wait: (self: PacketHandle) -> ...any,
    name: (self: PacketHandle) -> string,
    stats: (self: PacketHandle) -> Types.PacketStats,
}

function Packet.define(
    name: string,
    codec: Types.InternalCodec<any>,
    options: Types.PacketOptions?
): PacketHandle
    local opts = options or {} :: Types.PacketOptions
    local isUnreliable = opts.unreliable or false

    if isUnreliable and (codec :: any)._isDelta then
        error(`[Lync] Lync.packet: "{name}" cannot use delta codecs with unreliable transport`)
    end

    if (codec :: any)._hasUnknown and not opts.validate then
        warn(
            `[Lync] Lync.packet: "{name}" uses unknown codec without validate — data bypasses schema validation`
        )
    end

    if (codec :: any)._isDelta then
        Baseline.markHasDelta()
    end

    local tsMode, headerSize = resolveTimestampMode(opts.timestamp)
    local needsGate = IS_SERVER

    local openFn = Channel.resolveOpenFn(tsMode, name)

    local reg = Registry.register(
        name,
        Registry.KIND_PACKET,
        codec,
        isUnreliable,
        nil,
        needsGate,
        headerSize,
        tsMode,
        openFn,
        opts.rateLimit,
        opts.validate,
        opts.maxPayloadBytes
    )

    -- Lazy-require Server/Client to avoid circular dependency
    local _server: any = nil
    local _client: any = nil

    local function getServer(): any
        if not _server then
            _server = require(script.Parent.Parent.transport.Server)
        end
        return _server
    end

    local function getClient(): any
        if not _client then
            _client = require(script.Parent.Parent.transport.Client)
        end
        return _client
    end

    local handle: any = {}

    function handle.send(self: PacketHandle, data: any, target: any?): ()
        if Middleware.hasSendHooks() then
            data = Middleware.runSend(data, name, nil)
            if typeof(data) == "table" and (data :: any)._lyncDrop then
                return
            end
        end

        if IS_SERVER then
            if target == nil then
                error(`[Lync] Packet "{name}": server send requires a target`)
            end

            local Server = getServer()

            if typeof(target) == "Instance" and target:IsA("Player") then
                local ch = Server.getChannel(target :: Player, isUnreliable)
                Channel.writeBatch(ch, reg, data)
                if Channel.statsEnabled() then
                    reg.fires += 1
                end
                return
            end

            if typeof(target) == "table" then
                local tbl = target :: any

                if tbl._lyncAll then
                    for _, player in Players:GetPlayers() do
                        local ch = Server.getChannel(player, isUnreliable)
                        Channel.writeBatch(ch, reg, data)
                    end
                    if Channel.statsEnabled() then
                        reg.fires += 1
                    end
                    return
                end

                if tbl._lyncExcept then
                    local excluded = tbl._excluded :: { [Player]: boolean }
                    for _, player in Players:GetPlayers() do
                        if not excluded[player] then
                            local ch = Server.getChannel(player, isUnreliable)
                            Channel.writeBatch(ch, reg, data)
                        end
                    end
                    if Channel.statsEnabled() then
                        reg.fires += 1
                    end
                    return
                end

                if tbl._lyncGroup then
                    local members = tbl._members :: { [Player]: boolean }
                    for player in members do
                        local ch = Server.getChannel(player, isUnreliable)
                        Channel.writeBatch(ch, reg, data)
                    end
                    if Channel.statsEnabled() then
                        reg.fires += 1
                    end
                    return
                end

                for _, player in tbl do
                    if typeof(player) == "Instance" and (player :: Instance):IsA("Player") then
                        local ch = Server.getChannel(player :: Player, isUnreliable)
                        Channel.writeBatch(ch, reg, data)
                    end
                end
                if Channel.statsEnabled() then
                    reg.fires += 1
                end
                return
            end

            error(
                `[Lync] Packet "{name}": expected Player, table, Group, or Lync.all as target, got {typeof(
                    target
                )}`
            )
        else
            local Client = getClient()
            local ch = Client.getChannel(isUnreliable)
            Channel.writeBatch(ch, reg, data)
            if Channel.statsEnabled() then
                reg.fires += 1
            end
        end
    end

    function handle.on(_self: PacketHandle, fn: (...any) -> ()): Types.Connection
        return reg.signal:connect(fn)
    end

    function handle.once(_self: PacketHandle, fn: (...any) -> ()): Types.Connection
        local conn: Types.Connection
        conn = reg.signal:connect(function(...: any)
            conn:disconnect()
            fn(...)
        end)
        return conn
    end

    function handle.wait(_self: PacketHandle): ...any
        return reg.signal:wait()
    end

    function handle.name(_self: PacketHandle): string
        return reg.name
    end

    function handle.stats(_self: PacketHandle): Types.PacketStats
        return {
            bytesSent = reg.bytesSent,
            bytesReceived = reg.bytesReceived,
            fires = reg.fires,
            recvFires = reg.recvFires,
            drops = reg.drops,
        }
    end

    return table.freeze(handle)
end

return table.freeze(Packet)


--!strict
--!optimize 2
-- Batched connection lifecycle management.

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

-- Public --------------------------------------------------------------

local Scope = {}

export type ScopeHandle = {
    on: (self: ScopeHandle, source: any, fn: (...any) -> ()) -> Types.Connection,
    once: (self: ScopeHandle, source: any, fn: (...any) -> ()) -> Types.Connection,
    add: (self: ScopeHandle, connection: any) -> (),
    destroy: (self: ScopeHandle) -> (),
}

local ScopeMeta = {}
ScopeMeta.__index = ScopeMeta

function ScopeMeta.on(self: any, source: any, fn: (...any) -> ()): Types.Connection
    local conn = source:on(fn)
    local conns = self._connections
    local idx = self._count + 1
    self._count = idx
    conns[idx] = conn
    return conn
end

function ScopeMeta.once(self: any, source: any, fn: (...any) -> ()): Types.Connection
    local conn = source:once(fn)
    local conns = self._connections
    local idx = self._count + 1
    self._count = idx
    conns[idx] = conn
    return conn
end

function ScopeMeta.add(self: any, connection: any): ()
    local idx = self._count + 1
    self._count = idx
    self._connections[idx] = connection
end

function ScopeMeta.destroy(self: any): ()
    local conns = self._connections
    local count = self._count

    for i = 1, count do
        local conn = conns[i]
        conns[i] = nil :: any

        --[[
            Support both Lync Connection (conn:disconnect())
            and RBXScriptConnection (conn:Disconnect()).
        ]]
        if typeof(conn) == "table" then
            if conn.disconnect then
                conn:disconnect()
            end
        elseif typeof(conn) == "RBXScriptConnection" then
            (conn :: RBXScriptConnection):Disconnect()
        end
    end

    self._count = 0
end

function Scope.create(): ScopeHandle
    return setmetatable({
        _connections = {},
        _count = 0,
    }, ScopeMeta) :: any
end

return table.freeze(Scope)


--!strict
--!optimize 2
-- O(1) disconnect signal system with thread reuse.

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

-- Constants -----------------------------------------------------------

local STATE_DISCONNECTED = 0
local STATE_CONNECTED = 1
local STATE_ONCE = 2

-- State ---------------------------------------------------------------

local _freeThread: thread? = nil

-- Private -------------------------------------------------------------

local function passer(fn: (...any) -> (), ...): ()
    local thread = _freeThread
    _freeThread = nil
    fn(...)
    _freeThread = thread
end

local function yielder(): ()
    while true do
        passer(coroutine.yield())
    end
end

local function spawnCallback(fn: (...any) -> (), ...: any): ()
    if not _freeThread then
        _freeThread = coroutine.create(yielder)
        coroutine.resume(_freeThread :: thread)
    end
    task.spawn(_freeThread :: thread, fn, ...)
end

-- Entry type
type Entry = {
    fn: (...any) -> (),
    _state: number,
    connected: boolean,
    _signalEntries: { Entry },
    _signalCount: { number },
    _index: number,
}

-- EntryImpl metatable
local EntryImpl = {}
EntryImpl.__index = EntryImpl

function EntryImpl.disconnect(self: Entry): ()
    if self._state == STATE_DISCONNECTED then
        return
    end

    self._state = STATE_DISCONNECTED
    self.connected = false

    local entries = self._signalEntries
    local countRef = self._signalCount
    local idx = self._index
    local last = countRef[1]

    if idx < last then
        -- Swap-remove: move last entry to this slot
        local moved = entries[last]
        entries[idx] = moved
        moved._index = idx
    end

    entries[last] = nil :: any
    countRef[1] = last - 1
end

table.freeze(EntryImpl)

-- Public --------------------------------------------------------------

local SignalModule = {}

type SignalFields = {
    _entries: { Entry },
    _count: { number },
    _snapshot: { Entry },
}

local SignalImpl = {}
SignalImpl.__index = SignalImpl

function SignalImpl.connect(self: SignalFields, callback: (...any) -> ()): Types.Connection
    local countRef = self._count
    local idx = countRef[1] + 1
    countRef[1] = idx

    local entry: Entry = setmetatable({
        fn = callback,
        _state = STATE_CONNECTED,
        connected = true,
        _signalEntries = self._entries,
        _signalCount = countRef,
        _index = idx,
    }, EntryImpl) :: any

    self._entries[idx] = entry
    return entry :: any
end

function SignalImpl.once(self: SignalFields, callback: (...any) -> ()): Types.Connection
    local countRef = self._count
    local idx = countRef[1] + 1
    countRef[1] = idx

    local entry: Entry = setmetatable({
        fn = callback,
        _state = STATE_ONCE,
        connected = true,
        _signalEntries = self._entries,
        _signalCount = countRef,
        _index = idx,
    }, EntryImpl) :: any

    self._entries[idx] = entry
    return entry :: any
end

function SignalImpl.wait(self: SignalFields): ...any
    local running = coroutine.running()
    local conn: Types.Connection

    conn = SignalImpl.connect(self, function(...: any)
        conn:disconnect()
        task.spawn(running, ...)
    end)

    return coroutine.yield()
end

function SignalImpl.fire(self: SignalFields, ...: any): ()
    local count = self._count[1]
    if count == 0 then
        return
    end

    local entries = self._entries

    -- Single-listener fast path
    if count == 1 then
        local entry = entries[1]
        if entry._state == STATE_ONCE then
            EntryImpl.disconnect(entry)
            spawnCallback(entry.fn, ...)
        elseif entry._state == STATE_CONNECTED then
            spawnCallback(entry.fn, ...)
        end
        return
    end

    -- Multi-listener: snapshot to handle mid-fire disconnects
    local snapshot = self._snapshot
    table.move(entries, 1, count, 1, snapshot)

    for i = 1, count do
        local entry = snapshot[i]
        snapshot[i] = nil :: any

        if entry._state == STATE_ONCE then
            EntryImpl.disconnect(entry)
            spawnCallback(entry.fn, ...)
        elseif entry._state == STATE_CONNECTED then
            spawnCallback(entry.fn, ...)
        end
    end
end

function SignalImpl.fireSync(self: SignalFields, ...: any): ()
    local count = self._count[1]
    if count == 0 then
        return
    end

    local entries = self._entries

    if count == 1 then
        local entry = entries[1]
        if entry._state == STATE_ONCE then
            EntryImpl.disconnect(entry)
            entry.fn(...)
        elseif entry._state == STATE_CONNECTED then
            entry.fn(...)
        end
        return
    end

    local snapshot = self._snapshot
    table.move(entries, 1, count, 1, snapshot)

    for i = 1, count do
        local entry = snapshot[i]
        snapshot[i] = nil :: any

        if entry._state == STATE_ONCE then
            EntryImpl.disconnect(entry)
            entry.fn(...)
        elseif entry._state == STATE_CONNECTED then
            entry.fn(...)
        end
    end
end

table.freeze(SignalImpl)

function SignalModule.create(): any
    return setmetatable({
        _entries = {},
        _count = { 0 },
        _snapshot = {},
    }, SignalImpl)
end

return table.freeze(SignalModule)


--!strict
--!optimize 2
-- Fixed-size codec factory with buffer allocation.

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

-- Constants -----------------------------------------------------------

local DEFAULT_MAX = 262144

-- State ---------------------------------------------------------------

local _maxSize = DEFAULT_MAX

-- Private -------------------------------------------------------------

local countlz = bit32.countlz
local lshift = bit32.lshift

local function alloc(ch: Types.ChannelState, bytes: number): ()
    local needed = ch.cursor + bytes
    if needed <= ch.size then
        return
    end

    if needed > _maxSize then
        local name = ch.currentPacket
        error(
            if name
                then `[Lync] Channel: buffer overflow ({needed}B, max {_maxSize}B) in "{name}"`
                else `[Lync] Channel: buffer overflow ({needed}B, max {_maxSize}B)`
        )
    end

    local newSize = lshift(1, 32 - countlz(needed - 1))
    local newBuff = buffer.create(newSize)
    buffer.copy(newBuff, 0, ch.buff, 0, ch.cursor)
    ch.buff = newBuff
    ch.size = newSize
end

-- Public --------------------------------------------------------------

local Base = {}

function Base.alloc(ch: Types.ChannelState, bytes: number): ()
    alloc(ch, bytes)
end

function Base.setMaxSize(bytes: number): ()
    _maxSize = bytes
end

function Base.define<T>(
    size: number,
    directWrite: (b: buffer, offset: number, value: T) -> (),
    directRead: (b: buffer, offset: number) -> T,
    meta: { [string]: any }?
): Types.InternalCodec<T>
    local codec: any = {
        _size = size,
        _directWrite = directWrite,
        _directRead = directRead,

        write = function(ch: Types.ChannelState, value: T): ()
            local cursor = ch.cursor
            if cursor + size > ch.size then
                alloc(ch, size)
            end
            directWrite(ch.buff, cursor, value)
            ch.cursor = cursor + size
        end,

        read = function(src: buffer, pos: number, _refs: { Instance }?): (T, number)
            return directRead(src, pos), size
        end,
    }

    if meta then
        for key, val in meta do
            codec[key] = val
        end
    end

    return table.freeze(codec)
end

function Base.zero(): Types.InternalCodec<nil>
    return table.freeze({
        _size = 0,
        _directWrite = function(_b: buffer, _off: number, _v: nil): () end,
        _directRead = function(_b: buffer, _off: number): nil
            return nil
        end,
        write = function(_ch: Types.ChannelState, _v: nil): () end,
        read = function(_src: buffer, _pos: number, _refs: { Instance }?): (nil, number)
            return nil, 0
        end,
    } :: any)
end

return table.freeze(Base)


--!strict
--!native
-- array and deltaArray codecs.

local Base = require(script.Parent.Parent.Base)
local Shared = require(script.Parent.Shared)
local Types = require(script.Parent.Parent.Parent.Types)
local Varint = require(script.Parent.Parent.primitive.Varint)

-- Private -------------------------------------------------------------

local alloc = Base.alloc
local writeu8 = buffer.writeu8
local readu8 = buffer.readu8
local band = bit32.band
local bor = bit32.bor
local lshift = bit32.lshift

local acquireScratch = Shared.acquireScratch
local releaseScratch = Shared.releaseScratch
local rangeEqual = Shared.rangeEqual
local allocDeltaId = Shared.allocDeltaId

local DELTA_UNCHANGED = Shared.DELTA_UNCHANGED
local DELTA_FULL_OFFSET = Shared.DELTA_FULL_OFFSET
local DELTA_MULTI_OFFSET = Shared.DELTA_MULTI_OFFSET

-- Public --------------------------------------------------------------

local Array = {}

function Array.array(element: Types.InternalCodec<any>, maxCount: number?): Types.InternalCodec<any>
    local elemAny = element :: any

    -- Bool array: bitpacked
    if elemAny._isBool then
        return table.freeze({
            write = function(ch: Types.ChannelState, value: { any }): ()
                local len = #value
                Varint.write(ch, len)
                if len == 0 then
                    return
                end

                local byteCount = (len + 7) // 8
                local cursor = ch.cursor
                if cursor + byteCount > ch.size then
                    alloc(ch, byteCount)
                end
                local b = ch.buff

                local idx = 1
                for byteIdx = 0, byteCount - 1 do
                    local packed = 0
                    local limit = if idx + 8 <= len + 1 then 8 else len - idx + 1
                    for bit = 0, limit - 1 do
                        if value[idx] then
                            packed = bor(packed, lshift(1, bit))
                        end
                        idx += 1
                    end
                    writeu8(b, cursor + byteIdx, packed)
                end
                ch.cursor = cursor + byteCount
            end,

            read = function(src: buffer, pos: number, _refs: { Instance }?): ({ boolean }, number)
                local len, lenBytes = Varint.read(src, pos)
                if maxCount and len > maxCount then
                    error(`[Lync] Array: count {len} exceeds max {maxCount}`)
                end
                if len == 0 then
                    return {}, lenBytes
                end

                local byteCount = (len + 7) // 8
                local dataStart = pos + lenBytes
                local result = table.create(len)
                local idx = 1

                for byteIdx = 0, byteCount - 1 do
                    local byte = readu8(src, dataStart + byteIdx)
                    local limit = if idx + 8 <= len + 1 then 8 else len - idx + 1
                    for bit = 0, limit - 1 do
                        result[idx] = band(byte, lshift(1, bit)) ~= 0
                        idx += 1
                    end
                end

                return result, lenBytes + byteCount
            end,
        } :: any)
    end

    -- Direct array: element has _size + _directWrite + _directRead
    local elemSize = elemAny._size
    local elemDW = elemAny._directWrite
    local elemDR = elemAny._directRead

    if elemSize and elemDW and elemDR then
        return table.freeze({
            write = function(ch: Types.ChannelState, value: { any }): ()
                local len = #value
                Varint.write(ch, len)
                if len == 0 then
                    return
                end

                local payloadBytes = len * elemSize
                local cursor = ch.cursor
                if cursor + payloadBytes > ch.size then
                    alloc(ch, payloadBytes)
                end
                local b = ch.buff

                for i = 1, len do
                    elemDW(b, cursor + (i - 1) * elemSize, value[i])
                end
                ch.cursor = cursor + payloadBytes
            end,

            read = function(src: buffer, pos: number, _refs: { Instance }?): ({ any }, number)
                local len, lenBytes = Varint.read(src, pos)
                if maxCount and len > maxCount then
                    error(`[Lync] Array: count {len} exceeds max {maxCount}`)
                end
                if len == 0 then
                    return {}, lenBytes
                end

                local payloadBytes = len * elemSize
                local dataStart = pos + lenBytes

                -- Safety cap
                local remaining = buffer.len(src) - dataStart
                if payloadBytes > remaining then
                    len = remaining // elemSize
                    payloadBytes = len * elemSize
                end

                local result = table.create(len)
                for i = 1, len do
                    result[i] = elemDR(src, dataStart + (i - 1) * elemSize)
                end

                return result, lenBytes + payloadBytes
            end,
        } :: any)
    end

    -- Generic array
    return table.freeze({
        write = function(ch: Types.ChannelState, value: { any }): ()
            local len = #value
            Varint.write(ch, len)
            for i = 1, len do
                element.write(ch, value[i])
            end
        end,

        read = function(src: buffer, pos: number, refs: { Instance }?): ({ any }, number)
            local len, lenBytes = Varint.read(src, pos)
            if maxCount and len > maxCount then
                error(`[Lync] Array: count {len} exceeds max {maxCount}`)
            end
            if len == 0 then
                return {}, lenBytes
            end

            local result = table.create(len)
            local total = lenBytes
            for i = 1, len do
                local v, consumed = element.read(src, pos + total, refs)
                result[i] = v
                total += consumed
            end

            return result, total
        end,
    } :: any)
end

function Array.deltaArray(
    element: Types.InternalCodec<any>,
    maxCount: number?
): Types.InternalCodec<any>
    local deltaId = allocDeltaId()
    local innerArray = Array.array(element, maxCount)

    return table.freeze({
        _isDelta = true,

        write = function(ch: Types.ChannelState, value: { any }): ()
            local cache = ch.deltas[deltaId]

            if not cache then
                -- First frame: full
                local cursor = ch.cursor
                if cursor + 1 > ch.size then
                    alloc(ch, 1)
                end
                writeu8(ch.buff, cursor, 1) -- FLAG_FULL (len+1 where len=0 for first)
                ch.cursor = cursor + 1

                local scratch = acquireScratch()
                innerArray.write(scratch, value)
                local snapLen = scratch.cursor
                local snapBuf = buffer.create(snapLen)
                buffer.copy(snapBuf, 0, scratch.buff, 0, snapLen)

                ch.deltas[deltaId] = { raw = snapBuf, len = snapLen }
                innerArray.write(ch, value)
                releaseScratch()
                return
            end

            -- Compare against cache
            local scratch = acquireScratch()
            innerArray.write(scratch, value)
            local scratchLen = scratch.cursor
            local cachedLen = cache.len

            if
                scratchLen == cachedLen and rangeEqual(scratch.buff, 0, cache.raw, 0, scratchLen)
            then
                local cursor = ch.cursor
                if cursor + 1 > ch.size then
                    alloc(ch, 1)
                end
                writeu8(ch.buff, cursor, DELTA_UNCHANGED)
                ch.cursor = cursor + 1
                releaseScratch()
                return
            end

            -- Changed: full re-send
            local cursor = ch.cursor
            if cursor + 1 > ch.size then
                alloc(ch, 1)
            end
            writeu8(ch.buff, cursor, 1)
            ch.cursor = cursor + 1
            innerArray.write(ch, value)

            local newBuf = buffer.create(scratchLen)
            buffer.copy(newBuf, 0, scratch.buff, 0, scratchLen)
            cache.raw = newBuf
            cache.len = scratchLen

            releaseScratch()
        end,

        read = function(src: buffer, pos: number, refs: { Instance }?): ({ any }, number)
            local flag = readu8(src, pos)

            if flag == DELTA_UNCHANGED then
                return {} :: any, 1
            end

            local value, consumed = innerArray.read(src, pos + 1, refs)
            return value, 1 + consumed
        end,
    } :: any)
end

return table.freeze(Array)


--!strict
--!native
-- map and deltaMap codecs.

local Base = require(script.Parent.Parent.Base)
local Shared = require(script.Parent.Shared)
local Types = require(script.Parent.Parent.Parent.Types)
local Varint = require(script.Parent.Parent.primitive.Varint)

-- Private -------------------------------------------------------------

local alloc = Base.alloc
local writeu8 = buffer.writeu8
local readu8 = buffer.readu8

local acquireScratch = Shared.acquireScratch
local releaseScratch = Shared.releaseScratch
local rangeEqual = Shared.rangeEqual
local allocDeltaId = Shared.allocDeltaId
local DELTA_UNCHANGED = Shared.DELTA_UNCHANGED

-- Public --------------------------------------------------------------

local Map = {}

function Map.map(
    keyCodec: Types.InternalCodec<any>,
    valueCodec: Types.InternalCodec<any>,
    maxCount: number?
): Types.InternalCodec<any>
    return table.freeze({
        write = function(ch: Types.ChannelState, value: any): ()
            -- Count entries
            local count = 0
            for _ in value do
                count += 1
            end

            Varint.write(ch, count)
            if count == 0 then
                return
            end

            -- Pre-alloc if both fixed size
            local kAny = keyCodec :: any
            local vAny = valueCodec :: any
            if kAny._size and vAny._size then
                local entrySize = kAny._size + vAny._size
                local cursor = ch.cursor
                if cursor + count * entrySize > ch.size then
                    alloc(ch, count * entrySize)
                end
            end

            for k, v in value do
                keyCodec.write(ch, k)
                valueCodec.write(ch, v)
            end
        end,

        read = function(src: buffer, pos: number, refs: { Instance }?): (any, number)
            local count, lenBytes = Varint.read(src, pos)
            if maxCount and count > maxCount then
                error(`[Lync] Map: count {count} exceeds max {maxCount}`)
            end

            local result: { [any]: any } = {}
            local total = lenBytes

            for _ = 1, count do
                local k, kc = keyCodec.read(src, pos + total, refs)
                total += kc
                local v, vc = valueCodec.read(src, pos + total, refs)
                total += vc
                result[k] = v
            end

            return result, total
        end,
    } :: any)
end

function Map.deltaMap(
    keyCodec: Types.InternalCodec<any>,
    valueCodec: Types.InternalCodec<any>,
    maxCount: number?
): Types.InternalCodec<any>
    local deltaId = allocDeltaId()
    local innerMap = Map.map(keyCodec, valueCodec, maxCount)

    return table.freeze({
        _isDelta = true,

        write = function(ch: Types.ChannelState, value: any): ()
            local cache = ch.deltas[deltaId]

            if not cache then
                local cursor = ch.cursor
                if cursor + 1 > ch.size then
                    alloc(ch, 1)
                end
                writeu8(ch.buff, cursor, 3) -- FLAG_FULL
                ch.cursor = cursor + 1

                local scratch = acquireScratch()
                innerMap.write(scratch, value)
                local snapLen = scratch.cursor
                local snapBuf = buffer.create(snapLen)
                buffer.copy(snapBuf, 0, scratch.buff, 0, snapLen)

                ch.deltas[deltaId] = { raw = snapBuf, len = snapLen }
                innerMap.write(ch, value)
                releaseScratch()
                return
            end

            local scratch = acquireScratch()
            innerMap.write(scratch, value)
            local scratchLen = scratch.cursor
            local cachedLen = cache.len

            if
                scratchLen == cachedLen and rangeEqual(scratch.buff, 0, cache.raw, 0, scratchLen)
            then
                local cursor = ch.cursor
                if cursor + 1 > ch.size then
                    alloc(ch, 1)
                end
                writeu8(ch.buff, cursor, DELTA_UNCHANGED)
                ch.cursor = cursor + 1
                releaseScratch()
                return
            end

            -- Changed: full re-send
            local cursor = ch.cursor
            if cursor + 1 > ch.size then
                alloc(ch, 1)
            end
            writeu8(ch.buff, cursor, 3) -- FLAG_FULL
            ch.cursor = cursor + 1
            innerMap.write(ch, value)

            local newBuf = buffer.create(scratchLen)
            buffer.copy(newBuf, 0, scratch.buff, 0, scratchLen)
            cache.raw = newBuf
            cache.len = scratchLen

            releaseScratch()
        end,

        read = function(src: buffer, pos: number, refs: { Instance }?): (any, number)
            local flag = readu8(src, pos)

            if flag == DELTA_UNCHANGED then
                return {} :: any, 1
            end

            local value, consumed = innerMap.read(src, pos + 1, refs)
            return value, 1 + consumed
        end,
    } :: any)
end

return table.freeze(Map)


--!strict
--!optimize 2
-- Optional codec. 1 byte flag, value only if present.

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

-- Private -------------------------------------------------------------

local alloc = Base.alloc
local writeu8 = buffer.writeu8
local readu8 = buffer.readu8

-- Public --------------------------------------------------------------

local Optional = {}

function Optional.optional(inner: Types.InternalCodec<any>): Types.InternalCodec<any>
    local codec: any = {
        write = function(ch: Types.ChannelState, value: any): ()
            local cursor = ch.cursor
            if cursor + 1 > ch.size then
                alloc(ch, 1)
            end
            if value == nil then
                writeu8(ch.buff, cursor, 0)
                ch.cursor = cursor + 1
            else
                writeu8(ch.buff, cursor, 1)
                ch.cursor = cursor + 1
                inner.write(ch, value)
            end
        end,

        read = function(src: buffer, pos: number, refs: { Instance }?): (any, number)
            local flag = readu8(src, pos)
            if flag == 0 then
                return nil, 1
            end
            local value, consumed = inner.read(src, pos + 1, refs)
            return value, 1 + consumed
        end,
    }

    if (inner :: any)._hasUnknown then
        codec._hasUnknown = true
    end

    return table.freeze(codec)
end

return table.freeze(Optional)


--!strict
--!native
-- Shared helpers for composite codecs: delta flags, scratch pool, bool packing.

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

-- Constants -----------------------------------------------------------

local DELTA_UNCHANGED = 0
local DELTA_FULL_OFFSET = 1
local DELTA_MULTI_OFFSET = 2

-- State ---------------------------------------------------------------

local _nextDeltaId = 0
local _scratchDepth = 0
local _scratchStack: { Types.ChannelState } = {}

-- Private -------------------------------------------------------------

local alloc = Base.alloc
local band = bit32.band
local bor = bit32.bor
local bxor = bit32.bxor
local lshift = bit32.lshift
local rshift = bit32.rshift

local readu8 = buffer.readu8
local readu32 = buffer.readu32
local writeu8 = buffer.writeu8

-- Public --------------------------------------------------------------

local Shared = {}

Shared.DELTA_UNCHANGED = DELTA_UNCHANGED
Shared.DELTA_FULL_OFFSET = DELTA_FULL_OFFSET
Shared.DELTA_MULTI_OFFSET = DELTA_MULTI_OFFSET

function Shared.allocDeltaId(): number
    _nextDeltaId += 1
    return _nextDeltaId
end

function Shared.acquireScratch(): Types.ChannelState
    _scratchDepth += 1
    local ch = _scratchStack[_scratchDepth]
    if not ch then
        ch = {
            buff = buffer.create(1024),
            cursor = 0,
            size = 1024,
            refs = {},
            refCount = 0,
            lastId = -1,
            countPos = 0,
            itemCount = 0,
            singleMode = false,
            singlePos = 0,
            deltas = {},
            prevDump = nil,
            prevDumpLen = 0,
            currentPacket = nil,
        }
        _scratchStack[_scratchDepth] = ch
    else
        ch.cursor = 0
        ch.refCount = 0
    end
    return ch
end

function Shared.releaseScratch(): ()
    _scratchDepth -= 1
end

function Shared.rangeEqual(a: buffer, offA: number, b: buffer, offB: number, len: number): boolean
    -- u32 chunks
    local aligned = len - (len % 4)
    local i = 0
    while i < aligned do
        if bxor(readu32(a, offA + i), readu32(b, offB + i)) ~= 0 then
            return false
        end
        i += 4
    end

    -- u8 remainder
    while i < len do
        if readu8(a, offA + i) ~= readu8(b, offB + i) then
            return false
        end
        i += 1
    end

    return true
end

function Shared.extractFields(
    schema: { [string]: Types.InternalCodec<any> }
): ({ string }, { Types.InternalCodec<any> }, { string })
    local dataKeys: { string } = {}
    local dataCodecs: { Types.InternalCodec<any> } = {}
    local boolKeys: { string } = {}

    for key, codec in schema do
        if (codec :: any)._isBool then
            table.insert(boolKeys, key)
        else
            table.insert(dataKeys, key)
        end
    end

    if #dataKeys == 0 and #boolKeys == 0 then
        error("[Lync] Lync.struct: requires at least one field")
    end

    table.sort(dataKeys)
    table.sort(boolKeys)

    for i = 1, #dataKeys do
        dataCodecs[i] = schema[dataKeys[i]]
    end

    table.freeze(dataKeys)
    table.freeze(dataCodecs)
    table.freeze(boolKeys)

    return dataKeys, dataCodecs, boolKeys
end

function Shared.packBools(
    ch: Types.ChannelState,
    value: any,
    boolKeys: { string },
    boolCount: number
): ()
    local byteCount = (boolCount + 7) // 8
    local cursor = ch.cursor
    if cursor + byteCount > ch.size then
        alloc(ch, byteCount)
    end
    local b = ch.buff

    local idx = 1
    for byteIdx = 0, byteCount - 1 do
        local packed = 0
        local limit = if idx + 8 <= boolCount + 1 then 8 else boolCount - idx + 1
        for bit = 0, limit - 1 do
            if value[boolKeys[idx]] then
                packed = bor(packed, lshift(1, bit))
            end
            idx += 1
        end
        writeu8(b, cursor + byteIdx, packed)
    end

    ch.cursor = cursor + byteCount
end

function Shared.unpackBools(
    src: buffer,
    pos: number,
    boolKeys: { string },
    boolCount: number,
    result: any
): number
    local byteCount = (boolCount + 7) // 8
    local idx = 1

    for byteIdx = 0, byteCount - 1 do
        local byte = readu8(src, pos + byteIdx)
        local limit = if idx + 8 <= boolCount + 1 then 8 else boolCount - idx + 1
        for bit = 0, limit - 1 do
            result[boolKeys[idx]] = band(byte, lshift(1, bit)) ~= 0
            idx += 1
        end
    end

    return byteCount
end

return table.freeze(Shared)


--!strict
--!native
-- struct and deltaStruct codecs with single-field delta shortcut.

local Base = require(script.Parent.Parent.Base)
local Shared = require(script.Parent.Shared)
local Types = require(script.Parent.Parent.Parent.Types)

-- Private -------------------------------------------------------------

local alloc = Base.alloc
local writeu8 = buffer.writeu8
local readu8 = buffer.readu8
local extractFields = Shared.extractFields
local packBools = Shared.packBools
local unpackBools = Shared.unpackBools
local rangeEqual = Shared.rangeEqual
local acquireScratch = Shared.acquireScratch
local releaseScratch = Shared.releaseScratch
local allocDeltaId = Shared.allocDeltaId

local DELTA_UNCHANGED = Shared.DELTA_UNCHANGED
local DELTA_FULL_OFFSET = Shared.DELTA_FULL_OFFSET
local DELTA_MULTI_OFFSET = Shared.DELTA_MULTI_OFFSET

local band = bit32.band
local bor = bit32.bor
local lshift = bit32.lshift

-- Public --------------------------------------------------------------

local Struct = {}

function Struct.struct(schema: { [string]: Types.InternalCodec<any> }): Types.InternalCodec<any>
    local dataKeys, dataCodecs, boolKeys = extractFields(schema)
    local dataCount = #dataKeys
    local boolCount = #boolKeys
    local boolBytes = (boolCount + 7) // 8

    -- Check all-direct path
    local allDirect = true
    local fixedSize = 0

    for i = 1, dataCount do
        local c = dataCodecs[i] :: any
        if not c._size or not c._directWrite or not c._directRead then
            allDirect = false
            break
        end
        fixedSize += c._size
    end

    local totalFixed = fixedSize + boolBytes

    -- Freeze schema for Gate
    local frozenSchema: { [string]: Types.InternalCodec<any> } = {}
    for key, codec in schema do
        frozenSchema[key] = codec
    end
    table.freeze(frozenSchema)

    if allDirect and dataCount > 0 then
        -- Precompute offsets
        local offsets = table.create(dataCount)
        local directWrites = table.create(dataCount)
        local directReads = table.create(dataCount)
        local cursor = 0

        for i = 1, dataCount do
            local c = dataCodecs[i] :: any
            offsets[i] = cursor
            directWrites[i] = c._directWrite
            directReads[i] = c._directRead
            cursor += c._size
        end

        table.freeze(offsets)
        table.freeze(directWrites)
        table.freeze(directReads)

        return table.freeze({
            _size = totalFixed,
            _schema = frozenSchema,

            _directWrite = function(b: buffer, off: number, value: any): ()
                for i = 1, dataCount do
                    directWrites[i](b, off + offsets[i], value[dataKeys[i]])
                end
                -- Bool packing inline for direct path
                if boolCount > 0 then
                    local boolOff = off + fixedSize
                    local idx = 1
                    local byteCount = boolBytes
                    for byteIdx = 0, byteCount - 1 do
                        local packed = 0
                        local limit = if idx + 8 <= boolCount + 1 then 8 else boolCount - idx + 1
                        for bit = 0, limit - 1 do
                            if value[boolKeys[idx]] then
                                packed = bor(packed, lshift(1, bit))
                            end
                            idx += 1
                        end
                        buffer.writeu8(b, boolOff + byteIdx, packed)
                    end
                end
            end,

            _directRead = function(b: buffer, off: number): any
                local result: { [string]: any } = {}
                for i = 1, dataCount do
                    result[dataKeys[i]] = directReads[i](b, off + offsets[i])
                end
                if boolCount > 0 then
                    unpackBools(b, off + fixedSize, boolKeys, boolCount, result)
                end
                return result
            end,

            write = function(ch: Types.ChannelState, value: any): ()
                local c = ch.cursor
                if c + totalFixed > ch.size then
                    alloc(ch, totalFixed)
                end
                local b = ch.buff
                for i = 1, dataCount do
                    directWrites[i](b, c + offsets[i], value[dataKeys[i]])
                end
                ch.cursor = c + fixedSize
                if boolCount > 0 then
                    packBools(ch, value, boolKeys, boolCount)
                end
            end,

            read = function(src: buffer, pos: number, _refs: { Instance }?): (any, number)
                local result: { [string]: any } = {}
                for i = 1, dataCount do
                    result[dataKeys[i]] = directReads[i](src, pos + offsets[i])
                end
                if boolCount > 0 then
                    unpackBools(src, pos + fixedSize, boolKeys, boolCount, result)
                end
                return result, totalFixed
            end,
        } :: any)
    end

    -- Generic path
    return table.freeze({
        _schema = frozenSchema,

        write = function(ch: Types.ChannelState, value: any): ()
            for i = 1, dataCount do
                dataCodecs[i].write(ch, value[dataKeys[i]])
            end
            if boolCount > 0 then
                packBools(ch, value, boolKeys, boolCount)
            end
        end,

        read = function(src: buffer, pos: number, refs: { Instance }?): (any, number)
            local result: { [string]: any } = {}
            local total = 0
            for i = 1, dataCount do
                local v, consumed = dataCodecs[i].read(src, pos + total, refs)
                result[dataKeys[i]] = v
                total += consumed
            end
            if boolCount > 0 then
                total += unpackBools(src, pos + total, boolKeys, boolCount, result)
            end
            return result, total
        end,
    } :: any)
end

function Struct.deltaStruct(
    schema: { [string]: Types.InternalCodec<any> }
): Types.InternalCodec<any>
    local dataKeys, dataCodecs, boolKeys = extractFields(schema)
    local dataCount = #dataKeys
    local boolCount = #boolKeys
    local boolBytes = (boolCount + 7) // 8
    local segCount = dataCount + (if boolCount > 0 then 1 else 0)
    local maskBytes = (segCount + 7) // 8
    local deltaId = allocDeltaId()

    -- Freeze schema for Gate
    local frozenSchema: { [string]: Types.InternalCodec<any> } = {}
    for key, codec in schema do
        frozenSchema[key] = codec
    end
    table.freeze(frozenSchema)

    --[[
        Flag encoding with single-field shortcut:
          0            = UNCHANGED
          1..segCount  = single dirty segment index
          segCount + 1 = FULL
          segCount + 2 = MULTI dirty
    ]]
    local FLAG_FULL = segCount + DELTA_FULL_OFFSET
    local FLAG_MULTI = segCount + DELTA_MULTI_OFFSET

    -- Inner struct codec for full serialization
    local innerStruct = Struct.struct(schema)

    return table.freeze({
        _isDelta = true,
        _schema = frozenSchema,

        write = function(ch: Types.ChannelState, value: any): ()
            local cache = ch.deltas[deltaId]

            if not cache then
                -- First frame: FLAG_FULL + full payload
                local cursor = ch.cursor
                if cursor + 1 > ch.size then
                    alloc(ch, 1)
                end
                writeu8(ch.buff, cursor, FLAG_FULL)
                ch.cursor = cursor + 1

                -- Serialize to scratch for caching
                local scratch = acquireScratch()
                innerStruct.write(scratch, value)

                -- Snapshot scratch bytes as the baseline for future comparison
                local snapLen = scratch.cursor
                local snapBuf = buffer.create(snapLen)
                buffer.copy(snapBuf, 0, scratch.buff, 0, snapLen)

                ch.deltas[deltaId] = {
                    raw = snapBuf,
                    len = snapLen,
                }

                innerStruct.write(ch, value)
                releaseScratch()
                return
            end

            -- Compare current serialization against cached baseline
            local scratch = acquireScratch()
            innerStruct.write(scratch, value)

            local scratchBuf = scratch.buff
            local scratchLen = scratch.cursor
            local cachedBuf = cache.raw
            local cachedLen = cache.len

            -- Byte-for-byte match means no fields changed
            if scratchLen == cachedLen and rangeEqual(scratchBuf, 0, cachedBuf, 0, scratchLen) then
                local cursor = ch.cursor
                if cursor + 1 > ch.size then
                    alloc(ch, 1)
                end
                writeu8(ch.buff, cursor, DELTA_UNCHANGED)
                ch.cursor = cursor + 1
                releaseScratch()
                return
            end

            --[[
                Changed but not unchanged: full re-send. Per-segment delta
                tracking is deferred to a future version.
            ]]
            local cursor = ch.cursor
            if cursor + 1 > ch.size then
                alloc(ch, 1)
            end
            writeu8(ch.buff, cursor, FLAG_FULL)
            ch.cursor = cursor + 1
            innerStruct.write(ch, value)

            -- Update cache
            local newBuf = buffer.create(scratchLen)
            buffer.copy(newBuf, 0, scratchBuf, 0, scratchLen)
            cache.raw = newBuf
            cache.len = scratchLen

            releaseScratch()
        end,

        read = function(src: buffer, pos: number, refs: { Instance }?): (any, number)
            local flag = readu8(src, pos)

            if flag == DELTA_UNCHANGED then
                -- Return cached value (caller must have baseline)
                return nil, 1
            end

            if flag == FLAG_FULL then
                local value, consumed = innerStruct.read(src, pos + 1, refs)
                return value, 1 + consumed
            end

            -- For now, treat any other flag as full (future: per-segment delta)
            local value, consumed = innerStruct.read(src, pos + 1, refs)
            return value, 1 + consumed
        end,
    } :: any)
end

return table.freeze(Struct)


--!strict
--!optimize 2
-- Discriminated union codec. u8 variant tag + variant payload.

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

-- Private -------------------------------------------------------------

local alloc = Base.alloc
local writeu8 = buffer.writeu8
local readu8 = buffer.readu8

-- Public --------------------------------------------------------------

local Tagged = {}

function Tagged.define(
    tagField: string,
    variants: { [string]: Types.InternalCodec<any> }
): Types.InternalCodec<any>
    if tagField == "" then
        error("[Lync] Lync.tagged: tagField cannot be empty")
    end

    local names: { string } = {}
    for name in variants do
        table.insert(names, name)
    end
    table.sort(names)

    local variantCount = #names
    if variantCount == 0 then
        error("[Lync] Lync.tagged: requires at least one variant")
    end
    if variantCount > 256 then
        error("[Lync] Lync.tagged: exceeds 256 variants")
    end

    -- Build lookup tables
    local nameToTag: { [string]: number } = {}
    local tagToCodec = table.create(variantCount)
    local tagToName = table.create(variantCount)
    local hasUnknown = false

    for i = 1, variantCount do
        local name = names[i]
        local codec = variants[name]
        nameToTag[name] = i - 1
        tagToCodec[i] = codec
        tagToName[i] = name
        if (codec :: any)._hasUnknown then
            hasUnknown = true
        end
    end

    table.freeze(nameToTag)
    table.freeze(tagToCodec)
    table.freeze(tagToName)

    local codec: any = {
        write = function(ch: Types.ChannelState, value: any): ()
            local name = value[tagField]
            if name == nil then
                error(`[Lync] Tagged: missing tagField "{tagField}" in value`)
            end

            local tag = nameToTag[name]
            if tag == nil then
                error(`[Lync] Tagged: unknown variant "{name}"`)
            end

            local cursor = ch.cursor
            if cursor + 1 > ch.size then
                alloc(ch, 1)
            end
            writeu8(ch.buff, cursor, tag)
            ch.cursor = cursor + 1

            tagToCodec[tag + 1].write(ch, value)
        end,

        read = function(src: buffer, pos: number, refs: { Instance }?): (any, number)
            local tag = readu8(src, pos)
            local varCodec = tagToCodec[tag + 1]
            if not varCodec then
                error(`[Lync] Tagged: unknown tag {tag}`)
            end

            local name = tagToName[tag + 1]
            local data, consumed = varCodec.read(src, pos + 1, refs)

            -- Caller needs the tag field to discriminate the union
            if type(data) == "table" then
                data[tagField] = name
            end

            return data, 1 + consumed
        end,
    }

    if hasUnknown then
        codec._hasUnknown = true
    end
    return table.freeze(codec)
end

return table.freeze(Tagged)