
RomCompiler = {}

RomCompiler._keywordMap = {
	["and"] = "AD",
	["break"] = "BK",
	["do"] = "DO",
	["else"] = "EL",
	["elseif"] = "EI",
	["end"] = "EN",
	["false"] = "FA",
	["for"] = "FR",
	["function"] = "FN",
	["goto"] = "GT",
	["if"] = "IF",
	["in"] = "IN",
	["local"] = "LC",
	["nil"] = "NL",
	["not"] = "NT",
	["or"] = "OR",
	["repeat"] = "RP",
	["return"] = "RT",
	["then"] = "TN",
	["true"] = "TR",
	["until"] = "UT",
	["while"] = "WH"
}

RomCompiler._keywords = {}
for k, _ in pairs(RomCompiler._keywordMap) do
	RomCompiler._keywords[k] = true
end

RomCompiler._engineCallbacks = {
	["start"] = true,
	["update"] = true,
}

RomCompiler._engineProperties = {
	--math
	["x"] = true, ["y"] = true, ["z"] = true, ["w"] = true,
	["data"] = true, ["position"] = true, ["rotation"] = true, ["scale"] = true,
	["rightAxis"] = true, ["upAxis"] = true, ["forwardAxis"] = true,
	["origin"] = true, ["direction"] = true, ["min"] = true, ["max"] = true,
	["hitPoint"] = true, ["hitDistance"] = true,
	
	--sprite
	["text"] = true, ["textOffset"] = true, ["textColor"] = true, ["textCharColors"] = true,
	["size"] = true, ["billboard"] = true, ["billboardPixelPerfect"] = true,
	["color"] = true, ["border"] = true, ["borderColor"] = true, ["roundCorner"] = true,
	["textureId"] = true, ["texturePos"] = true, ["textureSize"] = true,
	["modelFilename"] = true, ["keyframes"] = true, ["waves"] = true,
	["enabled"] = true, ["frequency"] = true, ["amplitude"] = true,

	--synth
	["sineOsc"] = true, ["sineOscFreq"] = true,
	["squareOsc"] = true, ["squareOscFreq"] = true,
	["sawOsc"] = true, ["sawOscFreq"] = true,
	["noise"] = true, ["reverb"] = true,
	["filterCutoff"] = true, ["filterResonance"] = true,
	["wavefold"] = true, ["wet"] = true, ["pitch"] = true,
	["level"] = true, ["active"] = true,
}


function RomCompiler._resolveImports(codeStr, seen)
	seen = seen or {}
	local result = {}

	for line in (codeStr .. "\n"):gmatch("(.-)\n") do
		local importPath = line:match("^%-%-import%s+(.+)$")
		if importPath then
			importPath = importPath:match("^%s*(.-)%s*$")
			if not seen[importPath] then
				seen[importPath] = true
				local content = loadFile(importPath, 0)
				if not content or content == "" then
					content = loadFile("sys/lua/" .. importPath, 0)
				end
				if content and content ~= "" then
					local resolved = RomCompiler._resolveImports(content, seen)
					result[#result + 1] = resolved
				end
			end
		else
			result[#result + 1] = line
		end
	end

	return table.concat(result, "\n")
end


function RomCompiler._tokenize(code)
	local tokens = {}
	local pos = 1
	local len = #code

	local function peek(offset)
		local p = pos + (offset or 0)
		if p > len then return "" end
		return code:sub(p, p)
	end

	local function cur()
		if pos > len then return "" end
		return code:sub(pos, pos)
	end

	local function isAlpha(c)
		return (c >= "a" and c <= "z") or (c >= "A" and c <= "Z") or c == "_"
	end

	local function isDigit(c)
		return c >= "0" and c <= "9"
	end

	local function isAlnum(c)
		return isAlpha(c) or isDigit(c)
	end

	local function readLongString(startPos)
		local eqCount = 0
		local p = startPos + 1
		while p <= len and code:sub(p, p) == "=" do
			eqCount = eqCount + 1
			p = p + 1
		end
		if p > len or code:sub(p, p) ~= "[" then
			return nil
		end
		p = p + 1
		local closePattern = "]" .. string.rep("=", eqCount) .. "]"
		local closeStart = code:find(closePattern, p, true)
		if not closeStart then
			return code:sub(startPos), len + 1
		end
		local closeEnd = closeStart + #closePattern - 1
		return code:sub(startPos, closeEnd), closeEnd + 1
	end

	while pos <= len do
		local c = cur()

		if c == "\n" or c == "\r" then
			if c == "\r" and peek(1) == "\n" then
				pos = pos + 2
			else
				pos = pos + 1
			end
			tokens[#tokens + 1] = {type = "newline", value = ""}

		elseif c == " " or c == "\t" then
			pos = pos + 1

		elseif c == "-" and peek(1) == "-" then
			if peek(2) == "[" then
				local longStr, nextPos = readLongString(pos + 2)
				if longStr then
					pos = nextPos
					goto continue
				end
			end

			local lineEnd = code:find("\n", pos)
			if not lineEnd then
				lineEnd = len + 1
			end
			local commentText = code:sub(pos, lineEnd - 1)
			local importPath = commentText:match("^%-%-import%s+(.+)$")
			if importPath then
				tokens[#tokens + 1] = {type = "import", value = importPath:match("^%s*(.-)%s*$")}
			end
			pos = lineEnd

		elseif c == "[" and (peek(1) == "[" or peek(1) == "=") then
			local result, nextPos = readLongString(pos)
			if result then
				tokens[#tokens + 1] = {type = "string", value = result}
				pos = nextPos
			else
				tokens[#tokens + 1] = {type = "punct", value = "["}
				pos = pos + 1
			end

		elseif c == "\"" or c == "'" then
			local quote = c
			local start = pos
			pos = pos + 1
			while pos <= len do
				local sc = cur()
				if sc == "\\" then
					pos = pos + 2
				elseif sc == quote then
					pos = pos + 1
					break
				else
					pos = pos + 1
				end
			end
			tokens[#tokens + 1] = {type = "string", value = code:sub(start, pos - 1)}

		elseif isDigit(c) or (c == "." and isDigit(peek(1))) then
			local start = pos
			if c == "0" and (peek(1) == "x" or peek(1) == "X") then
				pos = pos + 2
				while pos <= len and (isDigit(cur()) or (cur() >= "a" and cur() <= "f") or (cur() >= "A" and cur() <= "F")) do
					pos = pos + 1
				end
			else
				while pos <= len and isDigit(cur()) do
					pos = pos + 1
				end
				if pos <= len and cur() == "." and peek(1) ~= "." then
					pos = pos + 1
					while pos <= len and isDigit(cur()) do
						pos = pos + 1
					end
				end
				if pos <= len and (cur() == "e" or cur() == "E") then
					pos = pos + 1
					if pos <= len and (cur() == "+" or cur() == "-") then
						pos = pos + 1
					end
					while pos <= len and isDigit(cur()) do
						pos = pos + 1
					end
				end
			end
			tokens[#tokens + 1] = {type = "number", value = code:sub(start, pos - 1)}

		elseif isAlpha(c) then
			local start = pos
			while pos <= len and isAlnum(cur()) do
				pos = pos + 1
			end
			local word = code:sub(start, pos - 1)
			if RomCompiler._keywords[word] then
				tokens[#tokens + 1] = {type = "keyword", value = word}
			else
				tokens[#tokens + 1] = {type = "ident", value = word}
			end

		elseif c == "." then
			if peek(1) == "." then
				if peek(2) == "." then
					tokens[#tokens + 1] = {type = "vararg", value = "..."}
					pos = pos + 3
				else
					tokens[#tokens + 1] = {type = "op", value = ".."}
					pos = pos + 2
				end
			else
				tokens[#tokens + 1] = {type = "punct", value = "."}
				pos = pos + 1
			end

		elseif c == "=" then
			if peek(1) == "=" then
				tokens[#tokens + 1] = {type = "op", value = "=="}
				pos = pos + 2
			else
				tokens[#tokens + 1] = {type = "op", value = "="}
				pos = pos + 1
			end

		elseif c == "~" and peek(1) == "=" then
			tokens[#tokens + 1] = {type = "op", value = "~="}
			pos = pos + 2

		elseif c == "<" then
			if peek(1) == "=" then
				tokens[#tokens + 1] = {type = "op", value = "<="}
				pos = pos + 2
			else
				tokens[#tokens + 1] = {type = "op", value = "<"}
				pos = pos + 1
			end

		elseif c == ">" then
			if peek(1) == "=" then
				tokens[#tokens + 1] = {type = "op", value = ">="}
				pos = pos + 2
			else
				tokens[#tokens + 1] = {type = "op", value = ">"}
				pos = pos + 1
			end

		elseif c == "/" then
			if peek(1) == "/" then
				tokens[#tokens + 1] = {type = "op", value = "//"}
				pos = pos + 2
			else
				tokens[#tokens + 1] = {type = "op", value = "/"}
				pos = pos + 1
			end

		elseif c == "+" or c == "-" or c == "*" or c == "%" or c == "^" or c == "#" then
			tokens[#tokens + 1] = {type = "op", value = c}
			pos = pos + 1

		elseif c == "(" or c == ")" or c == "{" or c == "}" or c == "[" or c == "]"
			or c == "," or c == ";" or c == ":" then
			tokens[#tokens + 1] = {type = "punct", value = c}
			pos = pos + 1

		else
			pos = pos + 1
		end

		::continue::
	end

	return tokens
end


function RomCompiler._generateName(index)
	local name = ""
	while true do
		local remainder = (index - 1) % 26
		name = string.char(97 + remainder) .. name
		index = math.floor((index - 1) / 26)
		if index == 0 then break end
	end
	return name
end


function RomCompiler._isFieldAccess(tokens, i)
	for j = i - 1, 1, -1 do
		if tokens[j].type ~= "newline" then
			return tokens[j].type == "punct" and
				(tokens[j].value == "." or tokens[j].value == ":")
		end
	end
	return false
end


function RomCompiler._collectDefinitions(tokens)
	local defs = {}

	local function nextNonNewline(from)
		for j = from, #tokens do
			if tokens[j].type ~= "newline" then return j end
		end
		return nil
	end

	local braceDepth = 0

	for i = 1, #tokens do
		local tok = tokens[i]

		if tok.type == "punct" and tok.value == "{" then
			braceDepth = braceDepth + 1
		elseif tok.type == "punct" and tok.value == "}" then
			braceDepth = math.max(0, braceDepth - 1)
		end

		if tok.type == "keyword" and tok.value == "local" then
			local ni = nextNonNewline(i + 1)
			if ni then
				if tokens[ni].type == "keyword" and tokens[ni].value == "function" then
					local fi = nextNonNewline(ni + 1)
					if fi and tokens[fi].type == "ident" then
						defs[tokens[fi].value] = true
					end
				else
					local j = ni
					while j and tokens[j].type == "ident" do
						defs[tokens[j].value] = true
						j = nextNonNewline(j + 1)
						if j and tokens[j].type == "punct" and tokens[j].value == "," then
							j = nextNonNewline(j + 1)
						else
							break
						end
					end
				end
			end

		elseif tok.type == "keyword" and tok.value == "function" then
			local prev = nil
			for j = i - 1, 1, -1 do
				if tokens[j].type ~= "newline" then
					prev = tokens[j]
					break
				end
			end
			local isLocal = prev and prev.type == "keyword" and prev.value == "local"

			local ni = nextNonNewline(i + 1)
			if ni and tokens[ni].type == "ident" then
				if not isLocal then
					local afterName = nextNonNewline(ni + 1)
					if afterName and tokens[afterName].type == "punct" and tokens[afterName].value == "(" then
						defs[tokens[ni].value] = true
					end
				end

				local j = ni
				while j do
					local nj = nextNonNewline(j + 1)
					if nj and tokens[nj].type == "punct" and (tokens[nj].value == "." or tokens[nj].value == ":") then
						local after = nextNonNewline(nj + 1)
						if after and tokens[after].type == "ident" then
							j = after
						else
							break
						end
					elseif nj and tokens[nj].type == "punct" and tokens[nj].value == "(" then
						local k = nextNonNewline(nj + 1)
						while k and tokens[k].type == "ident" do
							defs[tokens[k].value] = true
							k = nextNonNewline(k + 1)
							if k and tokens[k].type == "punct" and tokens[k].value == "," then
								k = nextNonNewline(k + 1)
							else
								break
							end
						end
						break
					else
						break
					end
				end
			elseif ni and tokens[ni].type == "punct" and tokens[ni].value == "(" then
				local k = nextNonNewline(ni + 1)
				while k and tokens[k].type == "ident" do
					defs[tokens[k].value] = true
					k = nextNonNewline(k + 1)
					if k and tokens[k].type == "punct" and tokens[k].value == "," then
						k = nextNonNewline(k + 1)
					else
						break
					end
				end
			end

		elseif tok.type == "keyword" and tok.value == "for" then
			local j = nextNonNewline(i + 1)
			while j and tokens[j].type == "ident" do
				defs[tokens[j].value] = true
				j = nextNonNewline(j + 1)
				if j and tokens[j].type == "punct" and tokens[j].value == "," then
					j = nextNonNewline(j + 1)
				else
					break
				end
			end

		elseif tok.type == "ident" and braceDepth == 0 and not RomCompiler._isFieldAccess(tokens, i) then
			local ni = nextNonNewline(i + 1)
			if ni and tokens[ni].type == "op" and tokens[ni].value == "=" then
				defs[tok.value] = true
			elseif ni and tokens[ni].type == "punct" and tokens[ni].value == "," then
				local j = nextNonNewline(ni + 1)
				while j do
					if tokens[j].type == "ident" then
						j = nextNonNewline(j + 1)
						if j and tokens[j].type == "op" and tokens[j].value == "=" then
							defs[tok.value] = true
							break
						elseif j and tokens[j].type == "punct" and tokens[j].value == "," then
							j = nextNonNewline(j + 1)
						else
							break
						end
					else
						break
					end
				end
			end
		end
	end

	for name in pairs(RomCompiler._engineCallbacks) do
		defs[name] = nil
	end

	return defs
end


function RomCompiler._buildNameMap(tokens)
	local definitions = RomCompiler._collectDefinitions(tokens)
	local nameMap = {}
	local counter = 0

	for i = 1, #tokens do
		local tok = tokens[i]
		if tok.type == "ident" and definitions[tok.value] and
			not RomCompiler._isFieldAccess(tokens, i) then
			if not nameMap[tok.value] then
				counter = counter + 1
				local name = RomCompiler._generateName(counter)
				while RomCompiler._keywords[name] do
					counter = counter + 1
					name = RomCompiler._generateName(counter)
				end
				nameMap[tok.value] = name
			end
		end
	end

	for i = 1, #tokens do
		local tok = tokens[i]
		if tok.type == "ident" and RomCompiler._isFieldAccess(tokens, i)
			and not RomCompiler._engineProperties[tok.value]
			and not RomCompiler._engineCallbacks[tok.value]
			and not nameMap[tok.value] then
			counter = counter + 1
			local name = RomCompiler._generateName(counter)
			while RomCompiler._keywords[name] do
				counter = counter + 1
				name = RomCompiler._generateName(counter)
			end
			nameMap[tok.value] = name
		end
	end

	return nameMap
end


function RomCompiler._applyTransforms(tokens, nameMap)
	local out = {}

	for i = 1, #tokens do
		local tok = tokens[i]
		if tok.type == "import" then
			goto skip
		elseif tok.type == "keyword" then
			out[#out + 1] = {type = "keyword", value = RomCompiler._keywordMap[tok.value]}
		elseif tok.type == "ident" then
			local mapped = nameMap[tok.value]
			local isField = RomCompiler._isFieldAccess(tokens, i)
			if mapped and (not isField or not RomCompiler._engineProperties[tok.value]) then
				out[#out + 1] = {type = "ident", value = mapped}
			else
				out[#out + 1] = {type = "ident", value = tok.value}
			end
		else
			out[#out + 1] = tok
		end
		::skip::
	end

	return out
end


function RomCompiler._isWord(tok)
	return tok.type == "keyword" or tok.type == "ident" or tok.type == "number"
end


function RomCompiler._formatOutput(tokens)
	local lines = {}
	local currentLine = {}
	local parenDepth = 0
	local bracketDepth = 0
	local braceDepth = 0
	local lastTok = nil
	local stmtBoundary = true

	local function flushLine()
		if #currentLine > 0 then
			local parts = {}
			for i = 1, #currentLine do
				local tok = currentLine[i]
				if i > 1 and RomCompiler._isWord(currentLine[i - 1]) and RomCompiler._isWord(tok) then
					parts[#parts + 1] = " "
				end
				parts[#parts + 1] = tok.value
			end
			lines[#lines + 1] = table.concat(parts)
			currentLine = {}
		end
	end

	local function isNested()
		return parenDepth > 0 or bracketDepth > 0 or braceDepth > 0
	end

	local stmtKeywords = {
		FN=true, IF=true, EI=true, FR=true, WH=true, RP=true,
		LC=true, RT=true, BK=true, GT=true,
		EN=true, EL=true, UT=true
	}

	local function shouldBreakBefore(tok)
		if isNested() then
			return false
		end

		if tok.type == "keyword" and stmtKeywords[tok.value] then
			if lastTok then
				if lastTok.type == "keyword" then
					local pv = lastTok.value
					if pv == "LC" or pv == "RT" then
						return false
					end
				end
				if lastTok.type == "op" and (lastTok.value == "=" or lastTok.value == "..") then
					return false
				end
			end
			return true
		end

		if tok.type == "ident" and stmtBoundary then
			return true
		end

		return false
	end

	for i = 1, #tokens do
		local tok = tokens[i]

		if tok.type == "newline" then
			if not isNested() then
				stmtBoundary = true
			end
			goto nextToken
		end

		if tok.type == "punct" then
			if tok.value == "(" then parenDepth = parenDepth + 1
			elseif tok.value == ")" then
				parenDepth = math.max(0, parenDepth - 1)
				if parenDepth == 0 and bracketDepth == 0 and braceDepth == 0 then
					stmtBoundary = true
				end
			elseif tok.value == "[" then bracketDepth = bracketDepth + 1
			elseif tok.value == "]" then bracketDepth = math.max(0, bracketDepth - 1)
			elseif tok.value == "{" then braceDepth = braceDepth + 1
			elseif tok.value == "}" then
				braceDepth = math.max(0, braceDepth - 1)
				if parenDepth == 0 and bracketDepth == 0 and braceDepth == 0 then
					stmtBoundary = true
				end
			end
		end

		if shouldBreakBefore(tok) then
			flushLine()
		end

		currentLine[#currentLine + 1] = tok
		lastTok = tok

		if tok.type == "keyword" then
			local v = tok.value
			if v == "EN" or v == "DO" or v == "TN" or v == "EL" or v == "RP" then
				stmtBoundary = true
			else
				stmtBoundary = false
			end
		elseif tok.type == "ident" or tok.type == "number" or tok.type == "string"
			or tok.type == "op" or tok.type == "vararg" then
			stmtBoundary = false
		elseif tok.type == "punct" then
			if tok.value == ";" then
				stmtBoundary = true
			elseif tok.value == "." or tok.value == ":" or tok.value == "," then
				stmtBoundary = false
			end
		end

		::nextToken::
	end

	flushLine()

	return table.concat(lines, "\n")
end


RomCompiler._b64chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

function RomCompiler._encodeBase64(data)
	local b64 = RomCompiler._b64chars
	local result = {}
	local len = #data
	local i = 1
	while i <= len do
		local a = string.byte(data, i) or 0
		local b = (i + 1 <= len) and string.byte(data, i + 1) or 0
		local c = (i + 2 <= len) and string.byte(data, i + 2) or 0
		local n = a * 65536 + b * 256 + c
		local remaining = len - i + 1
		result[#result + 1] = b64:sub(math.floor(n / 262144) % 64 + 1, math.floor(n / 262144) % 64 + 1)
		result[#result + 1] = b64:sub(math.floor(n / 4096) % 64 + 1, math.floor(n / 4096) % 64 + 1)
		if remaining >= 2 then
			result[#result + 1] = b64:sub(math.floor(n / 64) % 64 + 1, math.floor(n / 64) % 64 + 1)
		else
			result[#result + 1] = "="
		end
		if remaining >= 3 then
			result[#result + 1] = b64:sub(n % 64 + 1, n % 64 + 1)
		else
			result[#result + 1] = "="
		end
		i = i + 3
	end
	return table.concat(result)
end


function RomCompiler._compactTextData(content)
	local lines = {}
	for line in (content .. "\n"):gmatch("(.-)\n") do
		line = line:match("^%s*(.-)%s*$")
		if line ~= "" then
			line = line:gsub("%s*=%s*", "=")
			line = line:gsub("%s*,%s*", ",")
			line = line:gsub("(%d+%.%d%d%d%d%d+)", function(num)
				local n = tonumber(num)
				if n == 0.0 then return "0.0" end
				local formatted = string.format("%.4f", n)
				formatted = formatted:gsub("0+$", "")
				formatted = formatted:gsub("%.$", ".0")
				return formatted
			end)
			lines[#lines + 1] = line
		end
	end
	return table.concat(lines, "\n")
end


RomCompiler._excludeFiles = {
	["project.cfg"] = true,
	["tmp.gltf"] = true,
	["tmp.3dm"] = true,
	["game.pw"] = true
}

RomCompiler._excludeExtensions = {
	["png"] = true
}

RomCompiler._codeExtensions = {
	["lua"] = true
}

RomCompiler._modelExtensions = {
	["gltf"] = true
}

RomCompiler._textDataExtensions = {
	["anim"] = true,
	["level"] = true
}

RomCompiler._binaryExtensions = {
	["wav"] = true
}


function RomCompiler._getExtension(filename)
	return filename:match("%.([^%.]+)$") or ""
end


function RomCompiler._scanAssets()
	local textures = {}
	local models = {}
	local dataFiles = {}
	local binaryFiles = {}

	for pageId = 1, MEM_PAGES do
		textures[pageId] = getTextureBytes(pageId)

		local pagePath = "page" .. pageId
		local files = listFiles(pagePath)
		for _, filename in ipairs(files) do
			if not RomCompiler._excludeFiles[filename] then
				local ext = RomCompiler._getExtension(filename)
				if RomCompiler._excludeExtensions[ext] then
				elseif RomCompiler._modelExtensions[ext] then
					models[#models + 1] = {name = filename, pageId = pageId}
				elseif RomCompiler._binaryExtensions[ext] then
					binaryFiles[#binaryFiles + 1] = {name = filename, pageId = pageId}
				elseif RomCompiler._textDataExtensions[ext] then
					dataFiles[#dataFiles + 1] = {name = filename, pageId = pageId}
				elseif not RomCompiler._codeExtensions[ext] then
					dataFiles[#dataFiles + 1] = {name = filename, pageId = pageId}
				end
			end
		end
	end

	local rootFiles = listFiles("")
	for _, filename in ipairs(rootFiles) do
		if not RomCompiler._excludeFiles[filename] then
			local ext = RomCompiler._getExtension(filename)
			if RomCompiler._textDataExtensions[ext] then
				dataFiles[#dataFiles + 1] = {name = filename, pageId = 0}
			end
		end
	end

	return textures, models, dataFiles, binaryFiles
end


function RomCompiler._compileCode()
	local codeStr = loadFile("gameMain.lua", 0)
	if codeStr == "" then
		print("[RomCompiler] No gameMain.lua found")
		return nil
	end
	local resolved = RomCompiler._resolveImports(codeStr)
	local tokens = RomCompiler._tokenize(resolved)
	local nameMap = RomCompiler._buildNameMap(tokens)
	local transformed = RomCompiler._applyTransforms(tokens, nameMap)
	return RomCompiler._formatOutput(transformed)
end


function RomCompiler.compile()
	local textures, models, dataFiles, binaryFiles = RomCompiler._scanAssets()
	local code = RomCompiler._compileCode()
	if not code then return nil end

	local preamble = {}
	local pd = Main.projectData.video_ditherEnabled
	local bi = Main.projectData.video_bilinearInterpEnabled
	local c16 = Main.projectData.video_color16BitEnabled
	local da = Main.projectData.video_ditherTransparencyEnabled
	local cc = Main.projectData.video_colorCompressionEnabled
	if pd == nil then pd = false end
	if bi == nil then bi = false end
	if c16 == nil then c16 = false end
	if da == nil then da = false end
	if cc == nil then cc = false end
	if pd then
		local palStr = "{"
		local pal = Main.projectData.palette
		for i = 1, 24 do
			local c = pal[i]
			palStr = palStr .. "vec4(" .. c[1] .. "," .. c[2] .. "," .. c[3] .. "," .. c[4] .. ")"
			if i < 24 then palStr = palStr .. "," end
		end
		palStr = palStr .. "}"
		preamble[#preamble + 1] = "setDitherEnabled(TR," .. palStr .. ")"
	else
		preamble[#preamble + 1] = "setDitherEnabled(FA)"
	end
	preamble[#preamble + 1] = "setBilinearInterpEnabled(" .. (bi and "TR" or "FA") .. ")"
	preamble[#preamble + 1] = "setColor16BitEnabled(" .. (c16 and "TR" or "FA") .. ")"
	preamble[#preamble + 1] = "setDitherTransparencyEnabled(" .. (da and "TR" or "FA") .. ")"
	preamble[#preamble + 1] = "setColorCompressionEnabled(" .. (cc and "TR" or "FA") .. ")"
	code = table.concat(preamble, "\n") .. "\n" .. code

	local rom = {"::ROM::"}

	for pageId = 1, MEM_PAGES do
		if textures[pageId] and textures[pageId] ~= "" then
			rom[#rom + 1] = "::TEX:" .. pageId .. "::"
			rom[#rom + 1] = textures[pageId]
		end
	end

	for _, model in ipairs(models) do
		local glbBytes = compactModel(model.name, model.pageId)
		if glbBytes ~= "" then
			local glbName = model.name:gsub("%.[^%.]+$", ".glb")
			rom[#rom + 1] = "::MODEL:" .. glbName .. ":" .. model.pageId .. "::"
			rom[#rom + 1] = RomCompiler._encodeBase64(glbBytes)
		end
	end

	for _, file in ipairs(dataFiles) do
		local content = loadFile(file.name, file.pageId)
		if content ~= "" then
			local compacted = RomCompiler._compactTextData(content)
			rom[#rom + 1] = "::FILE:" .. file.name .. ":" .. file.pageId .. "::"
			rom[#rom + 1] = RomCompiler._encodeBase64(compacted)
		end
	end

	for _, file in ipairs(binaryFiles) do
		local content = loadFileBinary(file.name, file.pageId)
		if content ~= "" then
			rom[#rom + 1] = "::FILE:" .. file.name .. ":" .. file.pageId .. "::"
			rom[#rom + 1] = RomCompiler._encodeBase64(content)
		end
	end

	rom[#rom + 1] = "::CODE::"
	rom[#rom + 1] = code

	return table.concat(rom, "\n")
end
