--
--  Firewolf
--  Made by GravityScore and 1lann
--



--    Variables


local version = "3.5.4"
local build = 22

local w, h = term.getSize()

local isMenubarOpen = true
local menubarWindow = nil

local allowUnencryptedConnections = true
local enableTabBar = true

local currentWebsiteURL = ""
local builtInSites = {}

local currentProtocol = ""
local protocols = {}

local currentTab = 1
local maxTabs = 5
local maxTabNameWidth = 8
local tabs = {}

local languages = {}

local history = {}

local publicDNSChannel = 9999
local publicResponseChannel = 9998
local responseID = 41738

local httpTimeout = 10
local searchResultTimeout = 1
local initiationTimeout = 2
local animationInterval = 0.125
local fetchTimeout = 3
local serverLimitPerComputer = 1

local websiteErrorEvent = "firewolf_websiteErrorEvent"
local redirectEvent = "firewolf_redirectEvent"

local baseURL = "https://raw.githubusercontent.com/1lann/Firewolf/master/src"
local buildURL = baseURL .. "/build.txt"
local firewolfURL = baseURL .. "/client.lua"
local serverURL = baseURL .. "/server.lua"

local originalTerminal = term.current()

local firewolfLocation = "/" .. shell.getRunningProgram()
local downloadsLocation = "/downloads"


local theme = {}

local colorTheme = {
	background = colors.gray,
	accent = colors.red,
	subtle = colors.orange,

	lightText = colors.gray,
	text = colors.white,
	errorText = colors.red,
}

local grayscaleTheme = {
	background = colors.black,
	accent = colors.black,
	subtle = colors.black,

	lightText = colors.white,
	text = colors.white,
	errorText = colors.white,
}



--    Utilities


local modifiedRead = function(properties)
	local text = ""
	local startX, startY = term.getCursorPos()
	local pos = 0

	local previousText = ""
	local readHistory = nil
	local historyPos = 0

	if not properties then
		properties = {}
	end

	if properties.displayLength then
		properties.displayLength = math.min(properties.displayLength, w - 2)
	else
		properties.displayLength = w - startX - 1
	end

	if properties.startingText then
		text = properties.startingText
		pos = text:len()
	end

	if properties.history then
		readHistory = {}
		for k, v in pairs(properties.history) do
			readHistory[k] = v
		end
	end

	if readHistory[1] == text then
		table.remove(readHistory, 1)
	end

	local draw = function(replaceCharacter)
		local scroll = 0
		if properties.displayLength and pos > properties.displayLength then
			scroll = pos - properties.displayLength
		end

		local repl = replaceCharacter or properties.replaceCharacter
		term.setTextColor(theme.text)
		term.setCursorPos(startX, startY)
		if repl then
			term.write(string.rep(repl:sub(1, 1), text:len() - scroll))
		else
			term.write(text:sub(scroll + 1))
		end

		term.setCursorPos(startX + pos - scroll, startY)
	end

	term.setCursorBlink(true)
	draw()
	while true do
		local event, key, x, y, param4, param5 = os.pullEvent()

		if properties.onEvent then
			-- Actions:
			-- - exit (bool)
			-- - text
			-- - nullifyText

			term.setCursorBlink(false)
			local action = properties.onEvent(text, event, key, x, y, param4, param5)
			if action then
				if action.text then
					draw(" ")
					text = action.text
					pos = text:len()
				end if action.nullifyText then
					text = nil
					action.exit = true
				end if action.exit then
					break
				end
			end
			draw()
		end

		term.setCursorBlink(true)
		if event == "char" then
			local canType = true
			if properties.maxLength and text:len() >= properties.maxLength then
				canType = false
			end

			if canType then
				text = text:sub(1, pos) .. key .. text:sub(pos + 1, -1)
				pos = pos + 1
				draw()
			end
		elseif event == "key" then
			if key == keys.enter then
				break
			elseif key == keys.left and pos > 0 then
				pos = pos - 1
				draw()
			elseif key == keys.right and pos < text:len() then
				pos = pos + 1
				draw()
			elseif key == keys.backspace and pos > 0 then
				draw(" ")
				text = text:sub(1, pos - 1) .. text:sub(pos + 1, -1)
				pos = pos - 1
				draw()
			elseif key == keys.delete and pos < text:len() then
				draw(" ")
				text = text:sub(1, pos) .. text:sub(pos + 2, -1)
				draw()
			elseif key == keys.home then
				pos = 0
				draw()
			elseif key == keys["end"] then
				pos = text:len()
				draw()
			elseif (key == keys.up or key == keys.down) and readHistory then
				local shouldDraw = false
				if historyPos == 0 then
					previousText = text
				elseif historyPos > 0 then
					readHistory[historyPos] = text
				end

				if key == keys.up then
					if historyPos < #readHistory then
						historyPos = historyPos + 1
						shouldDraw = true
					end
				else
					if historyPos > 0 then
						historyPos = historyPos - 1
						shouldDraw = true
					end
				end

				if shouldDraw then
					draw(" ")
					if historyPos > 0 then
						text = readHistory[historyPos]
					else
						text = previousText
					end
					pos = text:len()
					draw()
				end
			end
		elseif event == "mouse_click" then
			local scroll = 0
			if properties.displayLength and pos > properties.displayLength then
				scroll = pos - properties.displayLength
			end

			if y == startY and x >= startX and x <= math.min(startX + text:len(), startX + (properties.displayLength or 10000)) then
				pos = x - startX + scroll
				draw()
			elseif y == startY then
				if x < startX then
					pos = scroll
					draw()
				elseif x > math.min(startX + text:len(), startX + (properties.displayLength or 10000)) then
					pos = text:len()
					draw()
				end
			end
		end
	end

	term.setCursorBlink(false)
	print("")
	return text
end


local prompt = function(items, x, y, w, h)
	local selected = 1
	local scroll = 0

	local draw = function()
		for i = scroll + 1, scroll + h do
			local item = items[i]
			if item then
				term.setCursorPos(x, y + i - 1)
				term.setBackgroundColor(theme.background)
				term.setTextColor(theme.lightText)

				if scroll + selected == i then
					term.setTextColor(theme.text)
					term.write(" > ")
				else
					term.write(" - ")
				end

				term.write(item)
			end
		end
	end

	draw()
	while true do
		local event, key, x, y = os.pullEvent()

		if event == "key" then
			if key == keys.up and selected > 1 then
				selected = selected - 1

				if selected - scroll == 0 then
					scroll = scroll - 1
				end
			elseif key == keys.down and selected < #items then
				selected = selected + 1
			end

			draw()
		elseif event == "mouse_click" then

		elseif event == "mouse_scroll" then
			if key > 0 then
				os.queueEvent("key", keys.down)
			else
				os.queueEvent("key", keys.up)
			end
		end
	end
end



--    GUI


local clear = function(bg, fg)
	term.setTextColor(fg)
	term.setBackgroundColor(bg)
	term.clear()
	term.setCursorPos(1, 1)
end


local fill = function(x, y, width, height, bg)
	term.setBackgroundColor(bg)
	for i = y, y + height - 1 do
		term.setCursorPos(x, i)
		term.write(string.rep(" ", width))
	end
end


local center = function(text)
	local x, y = term.getCursorPos()
	term.setCursorPos(math.floor(w / 2 - text:len() / 2) + (text:len() % 2 == 0 and 1 or 0), y)
	term.write(text)
	term.setCursorPos(1, y + 1)
end


local centerSplit = function(text, width)
	local words = {}
	for word in text:gmatch("[^ \t]+") do
		table.insert(words, word)
	end

	local lines = {""}
	while lines[#lines]:len() < width do
		lines[#lines] = lines[#lines] .. words[1] .. " "
		table.remove(words, 1)

		if #words == 0 then
			break
		end

		if lines[#lines]:len() + words[1]:len() >= width then
			table.insert(lines, "")
		end
	end

	for _, line in pairs(lines) do
		center(line)
	end
end



--    Updating


local download = function(url)
	http.request(url)
	local timeoutID = os.startTimer(httpTimeout)
	while true do
		local event, fetchedURL, response = os.pullEvent()
		if (event == "timer" and fetchedURL == timeoutID) or event == "http_failure" then
			return false
		elseif event == "http_success" and fetchedURL == url then
			local contents = response.readAll()
			response.close()
			return contents
		end
	end
end


local downloadAndSave = function(url, path)
	local contents = download(url)
	if contents and not fs.isReadOnly(path) and not fs.isDir(path) then
		local f = io.open(path, "w")
		f:write(contents)
		f:close()
		return false
	end
	return true
end


local updateAvailable = function()
	local number = download(buildURL)
	if not number then
		return false, true
	end

	if number and tonumber(number) and tonumber(number) > build then
		return true, false
	end

	return false, false
end


local redownloadBrowser = function()
	return downloadAndSave(firewolfURL, firewolfLocation)
end



--    Display Websites


builtInSites["display"] = {}


builtInSites["display"]["firewolf"] = function()
	local logo = {
		"______                         _  __ ",
		"|  ___|                       | |/ _|",
		"| |_  _ ____ _____      _____ | | |_ ",
		"|  _|| |  __/ _ \\ \\ /\\ / / _ \\| |  _|",
		"| |  | | | |  __/\\ V  V / <_> | | |  ",
		"\\_|  |_|_|  \\___| \\_/\\_/ \\___/|_|_|  ",
	}

	clear(theme.background, theme.text)
	fill(1, 3, w, 9, theme.subtle)

	term.setCursorPos(1, 3)
	for _, line in pairs(logo) do
		center(line)
	end

	term.setCursorPos(1, 10)
	center(version)

	term.setBackgroundColor(theme.background)
	term.setTextColor(theme.text)
	term.setCursorPos(1, 14)
	center("Search using the Query Box above")
	center("Visit rdnt://help for help using Firewolf.")

	term.setCursorPos(1, h - 2)
	center("Made by GravityScore and 1lann")
end


builtInSites["display"]["credits"] = function()
	clear(theme.background, theme.text)

	fill(1, 6, w, 3, theme.subtle)
	term.setCursorPos(1, 7)
	center("Credits")

	term.setBackgroundColor(theme.background)
	term.setCursorPos(1, 11)
	center("Written by GravityScore and 1lann")
	print("")
	center("RC4 Implementation by AgentE382")
end


builtInSites["display"]["help"] = function()
	clear(theme.background, theme.text)

	fill(1, 3, w, 3, theme.subtle)
	term.setCursorPos(1, 4)
	center("Help")

	term.setBackgroundColor(theme.background)
	term.setCursorPos(1, 7)
	center("Click on the URL bar or press control to")
	center("open the query box")
	print("")
	center("Type in a search query or website URL")
	center("into the query box.")
	print("")
	center("Search for nothing to see all available")
	center("websites.")
	print("")
	center("Visit rdnt://server to setup a server.")
	center("Visit rdnt://update to update Firewolf.")
end


builtInSites["display"]["server"] = function()
	clear(theme.background, theme.text)

	fill(1, 6, w, 3, theme.subtle)
	term.setCursorPos(1, 7)
	center("Server Software")

	term.setBackgroundColor(theme.background)
	term.setCursorPos(1, 11)
	if not http then
		center("HTTP is not enabled!")
		print("")
		center("Please enable it in your config file")
		center("to download Firewolf Server.")
	else
		center("Press space to download")
		center("Firewolf Server to:")
		print("")
		center("/fwserver")

		while true do
			local event, key = os.pullEvent()
			if event == "key" and key == keys.space then
				fill(1, 11, w, 4, theme.background)
				term.setCursorPos(1, 11)
				center("Downloading...")

				local err = downloadAndSave(serverURL, "/fwserver")

				fill(1, 11, w, 4, theme.background)
				term.setCursorPos(1, 11)
				center(err and "Download failed!" or "Download successful!")
			end
		end
	end
end


builtInSites["display"]["update"] = function()
	clear(theme.background, theme.text)

	fill(1, 3, w, 3, theme.subtle)
	term.setCursorPos(1, 4)
	center("Update")

	term.setBackgroundColor(theme.background)
	if not http then
		term.setCursorPos(1, 9)
		center("HTTP is not enabled!")
		print("")
		center("Please enable it in your config")
		center("file to download Firewolf updates.")
	else
		term.setCursorPos(1, 10)
		center("Checking for updates...")

		local available, err = updateAvailable()

		term.setCursorPos(1, 10)
		if available then
			term.clearLine()
			center("Update found!")
			center("Press enter to download.")

			while true do
				local event, key = os.pullEvent()
				if event == "key" and key == keys.enter then
					break
				end
			end

			fill(1, 10, w, 2, theme.background)
			term.setCursorPos(1, 10)
			center("Downloading...")

			local err = redownloadBrowser()

			term.setCursorPos(1, 10)
			term.clearLine()
			if err then
				center("Download failed!")
			else
				center("Download succeeded!")
				center("Please restart Firewolf...")
			end
		elseif err then
			term.clearLine()
			center("Checking failed!")
		else
			term.clearLine()
			center("No updates found.")
		end
	end
end



--    Built In Websites


builtInSites["error"] = function(err)
	fill(1, 3, w, 3, theme.subtle)
	term.setCursorPos(1, 4)
	center("Failed to load page!")

	term.setBackgroundColor(theme.background)
	term.setCursorPos(1, 9)
	center(err)
	print("")
	center("Please try again.")
end


builtInSites["noresults"] = function()
	fill(1, 3, w, 3, theme.subtle)
	term.setCursorPos(1, 4)
	center("No results!")

	term.setBackgroundColor(theme.background)
	term.setCursorPos(1, 9)
	center("Your search didn't return")
	center("any results!")

	os.pullEvent("key")
	os.queueEvent("")
	os.pullEvent()
end


builtInSites["search advanced"] = function(results)
	local startY = 6
	local height = h - startY - 1
	local scroll = 0

	local draw = function()
		fill(1, startY, w, height + 1, theme.background)

		for i = scroll + 1, scroll + height do
			if results[i] then
				term.setCursorPos(5, (i - scroll) + startY)
				term.write(currentProtocol .. "://" .. results[i])
			end
		end
	end

	draw()
	while true do
		local event, but, x, y = os.pullEvent()

		if event == "mouse_click" and y >= startY and y <= startY + height then
			local item = results[y - startY + scroll]
			if item then
				os.queueEvent(redirectEvent, item)
				coroutine.yield()
			end
		elseif event == "key" then
			if but == keys.up then
				scroll = math.max(0, scroll - 1)
			elseif but == keys.down and #results > height then
				scroll = math.min(scroll + 1, #results - height)
			end

			draw()
		elseif event == "mouse_scroll" then
			if but > 0 then
				os.queueEvent("key", keys.down)
			else
				os.queueEvent("key", keys.up)
			end
		end
	end
end


builtInSites["search basic"] = function(results)
	local startY = 6
	local height = h - startY - 1
	local scroll = 0
	local selected = 1

	local draw = function()
		fill(1, startY, w, height + 1, theme.background)

		for i = scroll + 1, scroll + height do
			if results[i] then
				if i == selected + scroll then
					term.setCursorPos(3, (i - scroll) + startY)
					term.write("> " .. currentProtocol .. "://" .. results[i])
				else
					term.setCursorPos(5, (i - scroll) + startY)
					term.write(currentProtocol .. "://" .. results[i])
				end
			end
		end
	end

	draw()
	while true do
		local event, but, x, y = os.pullEvent()

		if event == "key" then
			if but == keys.up and selected + scroll > 1 then
				if selected > 1 then
					selected = selected - 1
				else
					scroll = math.max(0, scroll - 1)
				end
			elseif but == keys.down and selected + scroll < #results then
				if selected < height then
					selected = selected + 1
				else
					scroll = math.min(scroll + 1, #results - height)
				end
			elseif but == keys.enter then
				local item = results[scroll + selected]
				if item then
					os.queueEvent(redirectEvent, item)
					coroutine.yield()
				end
			end

			draw()
		elseif event == "mouse_scroll" then
			if but > 0 then
				os.queueEvent("key", keys.down)
			else
				os.queueEvent("key", keys.up)
			end
		end
	end
end


builtInSites["search"] = function(results)
	clear(theme.background, theme.text)

	fill(1, 3, w, 3, theme.subtle)
	term.setCursorPos(1, 4)
	center(#results .. " Search " .. (#results == 1 and "Result" or "Results"))

	term.setBackgroundColor(theme.background)

	if term.isColor() then
		builtInSites["search advanced"](results)
	else
		builtInSites["search basic"](results)
	end
end


builtInSites["crash"] = function(err)
	fill(1, 3, w, 3, theme.subtle)
	term.setCursorPos(1, 4)
	center("The website crashed!")

	term.setBackgroundColor(theme.background)
	term.setCursorPos(1, 8)
	centerSplit(err, w - 4)
	print("\n")
	center("Please report this error to")
	center("the website creator.")
end



--    Menubar


local getTabName = function(url)
	local name = url:match("^[^/]+")

	if not name then
		name = "Search"
	end

	if name:sub(1, 3) == "www" then
		name = name:sub(5):gsub("^%s*(.-)%s*$", "%1")
	end

	if name:len() > maxTabNameWidth then
		name = name:sub(1, maxTabNameWidth):gsub("^%s*(.-)%s*$", "%1")
	end

	if name:sub(-1, -1) == "." then
		name = name:sub(1, -2):gsub("^%s*(.-)%s*$", "%1")
	end

	return name:gsub("^%s*(.-)%s*$", "%1")
end


local determineClickedTab = function(x, y)
	if y == 2 then
		local minx = 2
		for i, tab in pairs(tabs) do
			local name = getTabName(tab.url)

			if x >= minx and x <= minx + name:len() - 1 then
				return i
			elseif x == minx + name:len() and i == currentTab and #tabs > 1 then
				return "close"
			else
				minx = minx + name:len() + 2
			end
		end

		if x == minx and #tabs < maxTabs then
			return "new"
		end
	end

	return nil
end


local setupMenubar = function()
	if enableTabBar then
		menubarWindow = window.create(originalTerminal, 1, 1, w, 2, false)
	else
		menubarWindow = window.create(originalTerminal, 1, 1, w, 1, false)
	end
end


local drawMenubar = function()
	if isMenubarOpen then
		term.redirect(menubarWindow)
		menubarWindow.setVisible(true)

		fill(1, 1, w, 1, theme.accent)
		term.setTextColor(theme.text)

		term.setBackgroundColor(theme.accent)
		term.setCursorPos(2, 1)
		if currentWebsiteURL:match("^[^%?]+") then
			term.write(currentProtocol .. "://" .. currentWebsiteURL:match("^[^%?]+"))
		else
			term.write(currentProtocol .. "://" ..currentWebsiteURL)
		end

		term.setCursorPos(w - 5, 1)
		term.write("[===]")

		if enableTabBar then
			fill(1, 2, w, 1, theme.subtle)

			term.setCursorPos(1, 2)
			for i, tab in pairs(tabs) do
				term.setBackgroundColor(theme.subtle)
				term.setTextColor(theme.lightText)
				if i == currentTab then
					term.setTextColor(theme.text)
				end

				local tabName = getTabName(tab.url)
				term.write(" " .. tabName)

				if i == currentTab and #tabs > 1 then
					term.setTextColor(theme.errorText)
					term.write("x")
				else
					term.write(" ")
				end
			end

			if #tabs < maxTabs then
				term.setTextColor(theme.lightText)
				term.setBackgroundColor(theme.subtle)
				term.write(" + ")
			end
		end
	else
		menubarWindow.setVisible(false)
	end
end



--  RC4
--  Implementation by AgentE382


local cryptWrapper = function(plaintext, salt)
	local key = type(salt) == "table" and {unpack(salt)} or {string.byte(salt, 1, #salt)}
	local S = {}
	for i = 0, 255 do
		S[i] = i
	end

	local j, keylength = 0, #key
	for i = 0, 255 do
		j = (j + S[i] + key[i % keylength + 1]) % 256
		S[i], S[j] = S[j], S[i]
	end

	local i = 0
	j = 0
	local chars, astable = type(plaintext) == "table" and {unpack(plaintext)} or {string.byte(plaintext, 1, #plaintext)}, false

	for n = 1, #chars do
		i = (i + 1) % 256
		j = (j + S[i]) % 256
		S[i], S[j] = S[j], S[i]
		chars[n] = bit.bxor(S[(S[i] + S[j]) % 256], chars[n])
		if chars[n] > 127 or chars[n] == 13 then
			astable = true
		end
	end

	return astable and chars or string.char(unpack(chars))
end


local crypt = function(text, key)
	local resp, msg = pcall(cryptWrapper, text, key)
	if resp then
		return msg
	else
		return nil
	end
end



--  Base64
--
--  Base64 Encryption/Decryption
--  By KillaVanilla
--  http://www.computercraft.info/forums2/index.php?/topic/12450-killavanillas-various-apis/
--  http://pastebin.com/rCYDnCxn
--


local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"


local function sixBitToBase64(input)
	return string.sub(alphabet, input+1, input+1)
end


local function base64ToSixBit(input)
	for i=1, 64 do
		if input == string.sub(alphabet, i, i) then
			return i-1
		end
	end
end


local function octetToBase64(o1, o2, o3)
	local shifted = bit.brshift(bit.band(o1, 0xFC), 2)
	local i1 = sixBitToBase64(shifted)
	local i2 = "A"
	local i3 = "="
	local i4 = "="
	if o2 then
		i2 = sixBitToBase64(bit.bor( bit.blshift(bit.band(o1, 3), 4), bit.brshift(bit.band(o2, 0xF0), 4) ))
		if not o3 then
			i3 = sixBitToBase64(bit.blshift(bit.band(o2, 0x0F), 2))
		else
			i3 = sixBitToBase64(bit.bor( bit.blshift(bit.band(o2, 0x0F), 2), bit.brshift(bit.band(o3, 0xC0), 6) ))
		end
	else
		i2 = sixBitToBase64(bit.blshift(bit.band(o1, 3), 4))
	end
	if o3 then
		i4 = sixBitToBase64(bit.band(o3, 0x3F))
	end

	return i1..i2..i3..i4
end


local function base64ToThreeOctet(s1)
	local c1 = base64ToSixBit(string.sub(s1, 1, 1))
	local c2 = base64ToSixBit(string.sub(s1, 2, 2))
	local c3 = 0
	local c4 = 0
	local o1 = 0
	local o2 = 0
	local o3 = 0
	if string.sub(s1, 3, 3) == "=" then
		c3 = nil
		c4 = nil
	elseif string.sub(s1, 4, 4) == "=" then
		c3 = base64ToSixBit(string.sub(s1, 3, 3))
		c4 = nil
	else
		c3 = base64ToSixBit(string.sub(s1, 3, 3))
		c4 = base64ToSixBit(string.sub(s1, 4, 4))
	end
	o1 = bit.bor( bit.blshift(c1, 2), bit.brshift(bit.band( c2, 0x30 ), 4) )
	if c3 then
		o2 = bit.bor( bit.blshift(bit.band(c2, 0x0F), 4), bit.brshift(bit.band( c3, 0x3C ), 2) )
	else
		o2 = nil
	end
	if c4 then
		o3 = bit.bor( bit.blshift(bit.band(c3, 3), 6), c4 )
	else
		o3 = nil
	end
	return o1, o2, o3
end


local function splitIntoBlocks(bytes)
	local blockNum = 1
	local blocks = {}
	for i=1, #bytes, 3 do
		blocks[blockNum] = {bytes[i], bytes[i+1], bytes[i+2]}
		blockNum = blockNum+1
	end
	return blocks
end


function base64Encode(bytes)
	local blocks = splitIntoBlocks(bytes)
	local output = ""
	for i=1, #blocks do
		output = output..octetToBase64( unpack(blocks[i]) )
	end
	return output
end


function base64Decode(str)
	local bytes = {}
	local blocks = {}
	local blockNum = 1

	for i=1, #str, 4 do
		blocks[blockNum] = string.sub(str, i, i+3)
		blockNum = blockNum+1
	end

	for i=1, #blocks do
		local o1, o2, o3 = base64ToThreeOctet(blocks[i])
		table.insert(bytes, o1)
		table.insert(bytes, o2)
		table.insert(bytes, o3)
	end

	return bytes
end



--  SHA-256
--
--  Adaptation of the Secure Hashing Algorithm (SHA-244/256)
--  Found Here: http://lua-users.org/wiki/SecureHashAlgorithm
--
--  Using an adapted version of the bit library
--  Found Here: https://bitbucket.org/Boolsheet/bslf/src/1ee664885805/bit.lua


local MOD = 2^32
local MODM = MOD-1


local function memoize(f)
	local mt = {}
	local t = setmetatable({}, mt)
	function mt:__index(k)
		local v = f(k)
		t[k] = v
		return v
	end
	return t
end


local function make_bitop_uncached(t, m)
	local function bitop(a, b)
		local res,p = 0,1
		while a ~= 0 and b ~= 0 do
			local am, bm = a % m, b % m
			res = res + t[am][bm] * p
			a = (a - am) / m
			b = (b - bm) / m
			p = p * m
		end
		res = res + (a + b) * p
		return res
	end

	return bitop
end


local function make_bitop(t)
	local op1 = make_bitop_uncached(t,2^1)
	local op2 = memoize(function(a)
		return memoize(function(b)
			return op1(a, b)
		end)
	end)
	return make_bitop_uncached(op2, 2 ^ (t.n or 1))
end


local customBxor1 = make_bitop({[0] = {[0] = 0,[1] = 1}, [1] = {[0] = 1, [1] = 0}, n = 4})

local function customBxor(a, b, c, ...)
	local z = nil
	if b then
		a = a % MOD
		b = b % MOD
		z = customBxor1(a, b)
		if c then
			z = customBxor(z, c, ...)
		end
		return z
	elseif a then
		return a % MOD
	else
		return 0
	end
end


local function customBand(a, b, c, ...)
	local z
	if b then
		a = a % MOD
		b = b % MOD
		z = ((a + b) - customBxor1(a,b)) / 2
		if c then
			z = customBand(z, c, ...)
		end
		return z
	elseif a then
		return a % MOD
	else
		return MODM
	end
end


local function bnot(x)
	return (-1 - x) % MOD
end


local function rshift1(a, disp)
	if disp < 0 then
		return lshift(a, -disp)
	end
	return math.floor(a % 2 ^ 32 / 2 ^ disp)
end


local function rshift(x, disp)
	if disp > 31 or disp < -31 then
		return 0
	end
	return rshift1(x % MOD, disp)
end


local function lshift(a, disp)
	if disp < 0 then
		return rshift(a, -disp)
	end
	return (a * 2 ^ disp) % 2 ^ 32
end


local function rrotate(x, disp)
    x = x % MOD
    disp = disp % 32
    local low = customBand(x, 2 ^ disp - 1)
    return rshift(x, disp) + lshift(low, 32 - disp)
end


local k = {
	0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
	0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
	0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
	0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
	0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
	0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
	0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
	0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
	0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
	0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
	0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
	0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
	0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
	0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
	0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
	0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
}


local function str2hexa(s)
	return (string.gsub(s, ".", function(c)
		return string.format("%02x", string.byte(c))
	end))
end


local function num2s(l, n)
	local s = ""
	for i = 1, n do
		local rem = l % 256
		s = string.char(rem) .. s
		l = (l - rem) / 256
	end
	return s
end


local function s232num(s, i)
	local n = 0
	for i = i, i + 3 do
		n = n*256 + string.byte(s, i)
	end
	return n
end


local function preproc(msg, len)
	local extra = 64 - ((len + 9) % 64)
	len = num2s(8 * len, 8)
	msg = msg .. "\128" .. string.rep("\0", extra) .. len
	assert(#msg % 64 == 0)
	return msg
end


local function initH256(H)
	H[1] = 0x6a09e667
	H[2] = 0xbb67ae85
	H[3] = 0x3c6ef372
	H[4] = 0xa54ff53a
	H[5] = 0x510e527f
	H[6] = 0x9b05688c
	H[7] = 0x1f83d9ab
	H[8] = 0x5be0cd19
	return H
end


local function digestblock(msg, i, H)
	local w = {}
	for j = 1, 16 do
		w[j] = s232num(msg, i + (j - 1)*4)
	end
	for j = 17, 64 do
		local v = w[j - 15]
		local s0 = customBxor(rrotate(v, 7), rrotate(v, 18), rshift(v, 3))
		v = w[j - 2]
		w[j] = w[j - 16] + s0 + w[j - 7] + customBxor(rrotate(v, 17), rrotate(v, 19), rshift(v, 10))
	end

	local a, b, c, d, e, f, g, h = H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8]
	for i = 1, 64 do
		local s0 = customBxor(rrotate(a, 2), rrotate(a, 13), rrotate(a, 22))
		local maj = customBxor(customBand(a, b), customBand(a, c), customBand(b, c))
		local t2 = s0 + maj
		local s1 = customBxor(rrotate(e, 6), rrotate(e, 11), rrotate(e, 25))
		local ch = customBxor (customBand(e, f), customBand(bnot(e), g))
		local t1 = h + s1 + ch + k[i] + w[i]
		h, g, f, e, d, c, b, a = g, f, e, d + t1, c, b, a, t1 + t2
	end

	H[1] = customBand(H[1] + a)
	H[2] = customBand(H[2] + b)
	H[3] = customBand(H[3] + c)
	H[4] = customBand(H[4] + d)
	H[5] = customBand(H[5] + e)
	H[6] = customBand(H[6] + f)
	H[7] = customBand(H[7] + g)
	H[8] = customBand(H[8] + h)
end


local function sha256(msg)
	msg = preproc(msg, #msg)
	local H = initH256({})
	for i = 1, #msg, 64 do
		digestblock(msg, i, H)
	end
	return str2hexa(num2s(H[1], 4) .. num2s(H[2], 4) .. num2s(H[3], 4) .. num2s(H[4], 4) ..
		num2s(H[5], 4) .. num2s(H[6], 4) .. num2s(H[7], 4) .. num2s(H[8], 4))
end


local protocolName = "Firewolf"



--    Cryptography


local Cryptography = {}
Cryptography.sha = {}
Cryptography.base64 = {}
Cryptography.aes = {}


function Cryptography.bytesFromMessage(msg)
	local bytes = {}

	for i = 1, msg:len() do
		local letter = string.byte(msg:sub(i, i))
		table.insert(bytes, letter)
	end

	return bytes
end


function Cryptography.messageFromBytes(bytes)
	local msg = ""

	for i = 1, #bytes do
		local letter = string.char(bytes[i])
		msg = msg .. letter
	end

	return msg
end


function Cryptography.bytesFromKey(key)
	local bytes = {}

	for i = 1, key:len() / 2 do
		local group = key:sub((i - 1) * 2 + 1, (i - 1) * 2 + 1)
		local num = tonumber(group, 16)
		table.insert(bytes, num)
	end

	return bytes
end


function Cryptography.sha.sha256(msg)
	return sha256(msg)
end


function Cryptography.aes.encrypt(msg, key)
	return base64Encode(crypt(msg, key))
end


function Cryptography.aes.decrypt(msg, key)
	return crypt(base64Decode(msg), key)
end


function Cryptography.base64.encode(msg)
	return base64Encode(Cryptography.bytesFromMessage(msg))
end


function Cryptography.base64.decode(msg)
	return Cryptography.messageFromBytes(base64Decode(msg))
end


function Cryptography.channel(text)
	local hashed = Cryptography.sha.sha256(text)

	local total = 0

	for i = 1, hashed:len() do
		total = total + string.byte(hashed:sub(i, i))
	end

	return (total % 55530) + 10000
end


function Cryptography.sanatize(text)
	local sanatizeChars = {"%", "(", ")", "[", "]", ".", "+", "-", "*", "?", "^", "$"}

	for _, char in pairs(sanatizeChars) do
		text = text:gsub("%"..char, "%%%"..char)
	end
	return text
end



--  Modem


local Modem = {}
Modem.modems = {}


function Modem.exists()
	Modem.exists = false
	for _, side in pairs(rs.getSides()) do
		if peripheral.isPresent(side) and peripheral.getType(side) == "modem" then
			Modem.exists = true

			if not Modem.modems[side] then
				Modem.modems[side] = peripheral.wrap(side)
			end
		end
	end

	return Modem.exists
end


function Modem.open(channel)
	if not Modem.exists then
		return false
	end

	for side, modem in pairs(Modem.modems) do
		modem.open(channel)
		rednet.open(side)
	end

	return true
end


function Modem.close(channel)
	if not Modem.exists then
		return false
	end

	for side, modem in pairs(Modem.modems) do
		modem.close(channel)
	end

	return true
end


function Modem.closeAll()
	if not Modem.exists then
		return false
	end

	for side, modem in pairs(Modem.modems) do
		modem.closeAll()
	end

	return true
end


function Modem.isOpen(channel)
	if not Modem.exists then
		return false
	end

	local isOpen = false
	for side, modem in pairs(Modem.modems) do
		if modem.isOpen(channel) then
			isOpen = true
			break
		end
	end

	return isOpen
end


function Modem.transmit(channel, msg)
	if not Modem.exists then
		return false
	end

	if not Modem.isOpen(channel) then
		Modem.open(channel)
	end

	for side, modem in pairs(Modem.modems) do
		modem.transmit(channel, channel, msg)
	end

	return true
end



--    Handshake


local Handshake = {}

Handshake.prime = 625210769
Handshake.channel = 54569
Handshake.base = -1
Handshake.secret = -1
Handshake.sharedSecret = -1
Handshake.packetHeader = "["..protocolName.."-Handshake-Packet-Header]"
Handshake.packetMatch = "%["..protocolName.."%-Handshake%-Packet%-Header%](.+)"


function Handshake.exponentWithModulo(base, exponent, modulo)
	local remainder = base

	for i = 1, exponent-1 do
		remainder = remainder * remainder
		if remainder >= modulo then
			remainder = remainder % modulo
		end
	end

	return remainder
end


function Handshake.clear()
	Handshake.base = -1
	Handshake.secret = -1
	Handshake.sharedSecret = -1
end


function Handshake.generateInitiatorData()
	Handshake.base = math.random(10,99999)
	Handshake.secret = math.random(10,99999)
	return {
		type = "initiate",
		prime = Handshake.prime,
		base = Handshake.base,
		moddedSecret = Handshake.exponentWithModulo(Handshake.base, Handshake.secret, Handshake.prime)
	}
end


function Handshake.generateResponseData(initiatorData)
	local isPrimeANumber = type(initiatorData.prime) == "number"
	local isPrimeMatching = initiatorData.prime == Handshake.prime
	local isBaseANumber = type(initiatorData.base) == "number"
	local isInitiator = initiatorData.type == "initiate"
	local isModdedSecretANumber = type(initiatorData.moddedSecret) == "number"
	local areAllNumbersNumbers = isPrimeANumber and isBaseANumber and isModdedSecretANumber

	if areAllNumbersNumbers and isPrimeMatching then
		if isInitiator then
			Handshake.base = initiatorData.base
			Handshake.secret = math.random(10,99999)
			Handshake.sharedSecret = Handshake.exponentWithModulo(initiatorData.moddedSecret, Handshake.secret, Handshake.prime)
			return {
				type = "response",
				prime = Handshake.prime,
				base = Handshake.base,
				moddedSecret = Handshake.exponentWithModulo(Handshake.base, Handshake.secret, Handshake.prime)
			}, Handshake.sharedSecret
		elseif initiatorData.type == "response" and Handshake.base > 0 and Handshake.secret > 0 then
			Handshake.sharedSecret = Handshake.exponentWithModulo(initiatorData.moddedSecret, Handshake.secret, Handshake.prime)
			return Handshake.sharedSecret
		else
			return false
		end
	else
		return false
	end
end



--    Secure Connection


local SecureConnection = {}
SecureConnection.__index = SecureConnection


SecureConnection.packetHeaderA = "["..protocolName.."-"
SecureConnection.packetHeaderB = "-SecureConnection-Packet-Header]"
SecureConnection.packetMatchA = "%["..protocolName.."%-"
SecureConnection.packetMatchB = "%-SecureConnection%-Packet%-Header%](.+)"
SecureConnection.connectionTimeout = 0.1
SecureConnection.successPacketTimeout = 0.1


function SecureConnection.new(secret, key, identifier, distance, isRednet)
	local self = setmetatable({}, SecureConnection)
	self:setup(secret, key, identifier, distance, isRednet)
	return self
end


function SecureConnection:setup(secret, key, identifier, distance, isRednet)
	local rawSecret

	if isRednet then
		self.isRednet = true
		self.distance = -1
		self.rednet_id = distance
		rawSecret = protocolName .. "|" .. tostring(secret) .. "|" .. tostring(identifier) ..
		"|" .. tostring(key) .. "|rednet"
	else
		self.isRednet = false
		self.distance = distance
		rawSecret = protocolName .. "|" .. tostring(secret) .. "|" .. tostring(identifier) ..
		"|" .. tostring(key) .. "|" .. tostring(distance)
	end

	self.identifier = identifier
	self.packetMatch = SecureConnection.packetMatchA .. Cryptography.sanatize(identifier) .. SecureConnection.packetMatchB
	self.packetHeader = SecureConnection.packetHeaderA .. identifier .. SecureConnection.packetHeaderB
	self.secret = Cryptography.sha.sha256(rawSecret)
	self.channel = Cryptography.channel(self.secret)

	if not self.isRednet then
		Modem.open(self.channel)
	end
end


function SecureConnection:verifyHeader(msg)
	if type(msg) ~= "string" then return false end

	if msg:match(self.packetMatch) then
		return true
	else
		return false
	end
end


function SecureConnection:sendMessage(msg, rednetProtocol)
	local rawEncryptedMsg = Cryptography.aes.encrypt(self.packetHeader .. msg, self.secret)
	local encryptedMsg = self.packetHeader .. rawEncryptedMsg

	if self.isRednet then
		rednet.send(self.rednet_id, encryptedMsg, rednetProtocol)
		return true
	else
		return Modem.transmit(self.channel, encryptedMsg)
	end
end


function SecureConnection:decryptMessage(msg)
	if self:verifyHeader(msg) then
		local encrypted = msg:match(self.packetMatch)

		local unencryptedMsg = nil
		pcall(function() unencryptedMsg = Cryptography.aes.decrypt(encrypted, self.secret) end)
		if not unencryptedMsg then
			return false, "Could not decrypt"
		end

		if self:verifyHeader(unencryptedMsg) then
			return true, unencryptedMsg:match(self.packetMatch)
		else
			return false, "Could not verify"
		end
	else
		return false, "Could not stage 1 verify"
	end
end



--    RDNT Protocol


protocols["rdnt"] = {}

local header = {}
header.dnsPacket = "[Firewolf-DNS-Packet]"
header.dnsHeaderMatch = "^%[Firewolf%-DNS%-Response%](.+)$"
header.rednetHeader = "[Firewolf-Rednet-Channel-Simulation]"
header.rednetMatch = "^%[Firewolf%-Rednet%-Channel%-Simulation%](%d+)$"
header.responseMatchA = "^%[Firewolf%-"
header.responseMatchB = "%-"
header.responseMatchC = "%-Handshake%-Response%](.+)$"
header.requestHeaderA = "[Firewolf-"
header.requestHeaderB = "-Handshake-Request]"
header.pageRequestHeaderA = "[Firewolf-"
header.pageRequestHeaderB = "-Page-Request]"
header.pageResponseMatchA = "^%[Firewolf%-"
header.pageResponseMatchB = "%-Page%-Response%]%[HEADER%](.-)%[BODY%](.+)$"
header.closeHeaderA = "[Firewolf-"
header.closeHeaderB = "-Connection-Close]"


protocols["rdnt"]["setup"] = function()
	if not Modem.exists() then
		error("No modem found!")
	end
end


protocols["rdnt"]["fetchAllSearchResults"] = function()
	Modem.open(publicDNSChannel)
	Modem.open(publicResponseChannel)
	Modem.transmit(publicDNSChannel, header.dnsPacket)
	Modem.close(publicDNSChannel)

	rednet.broadcast(header.dnsPacket, header.rednetHeader .. publicDNSChannel)

	local uniqueServers = {}
	local uniqueDomains = {}

	local timer = os.startTimer(searchResultTimeout)

	while true do
		local event, id, channel, protocol, message, dist = os.pullEventRaw()
		if event == "modem_message" then
			if channel == publicResponseChannel and type(message) == "string" and message:match(header.dnsHeaderMatch) then
				if not uniqueServers[tostring(dist)] then
					uniqueServers[tostring(dist)] = true
					local domain = message:match(header.dnsHeaderMatch)
					if not uniqueDomains[domain] then
						if not(domain:find("/") or domain:find(":") or domain:find("%?")) and #domain > 4 then
							timer = os.startTimer(searchResultTimeout)
							uniqueDomains[message:match(header.dnsHeaderMatch)] = tostring(dist)
						end
					end
				end
			end
		elseif event == "rednet_message" and allowUnencryptedConnections then
			if protocol and tonumber(protocol:match(header.rednetMatch)) == publicResponseChannel and channel:match(header.dnsHeaderMatch) then
				if not uniqueServers[tostring(id)] then
					uniqueServers[tostring(id)] = true
					local domain = channel:match(header.dnsHeaderMatch)
					if not uniqueDomains[domain] then
						if not(domain:find("/") or domain:find(":") or domain:find("%?")) and #domain > 4 then
							timer = os.startTimer(searchResultTimeout)
							uniqueDomains[domain] = tostring(id)
						end
					end
				end
			end
		elseif event == "timer" and id == timer then
			local results = {}
			for k, _ in pairs(uniqueDomains) do
				table.insert(results, k)
			end

			return results
		end
	end
end


protocols["rdnt"]["fetchConnectionObject"] = function(url)
	local serverChannel = Cryptography.channel(url)
	local requestHeader = header.requestHeaderA .. url .. header.requestHeaderB
	local responseMatch = header.responseMatchA .. Cryptography.sanatize(url) .. header.responseMatchB

	local serializedHandshake = textutils.serialize(Handshake.generateInitiatorData())

	local rednetResults = {}
	local directResults = {}

	local disconnectOthers = function(ignoreDirect)
		for k,v in pairs(rednetResults) do
			v.close()
		end
		for k,v in pairs(directResults) do
			if k ~= ignoreDirect then
				v.close()
			end
		end
	end

	local timer = os.startTimer(initiationTimeout)

	Modem.open(serverChannel)
	Modem.transmit(serverChannel, requestHeader .. serializedHandshake)

	rednet.broadcast(requestHeader .. serializedHandshake, header.rednetHeader .. serverChannel)

	-- Extendable to have server selection

	while true do
		local event, id, channel, protocol, message, dist = os.pullEventRaw()
		if event == "modem_message" then
			local fullMatch = responseMatch .. tostring(dist) .. header.responseMatchC
			if channel == serverChannel and type(message) == "string" and message:match(fullMatch) and type(textutils.unserialize(message:match(fullMatch))) == "table" then
				local key = Handshake.generateResponseData(textutils.unserialize(message:match(fullMatch)))
				if key then
					local connection = SecureConnection.new(key, url, url, dist)
					table.insert(directResults, {
						connection = connection,
						fetchPage = function(page)
							if not connection then
								return nil
							end

							local fetchTimer = os.startTimer(fetchTimeout)

							local pageRequest = header.pageRequestHeaderA .. url .. header.pageRequestHeaderB .. page
							local pageResponseMatch = header.pageResponseMatchA .. Cryptography.sanatize(url) .. header.pageResponseMatchB

							connection:sendMessage(pageRequest, header.rednetHeader .. connection.channel)

							while true do
								local event, id, channel, protocol, message, dist = os.pullEventRaw()
								if event == "modem_message" and channel == connection.channel and type(message) == "string" and connection:verifyHeader(message) then
									local resp, data = connection:decryptMessage(message)
									if not resp then
										-- Decryption error
									elseif data and data ~= page then
										if data:match(pageResponseMatch) then
											local head, body = data:match(pageResponseMatch)
											return body, textutils.unserialize(head)
										end
									end
								elseif event == "timer" and id == fetchTimer then
									return nil
								end
							end
						end,
						close = function()
							if connection ~= nil then
								connection:sendMessage(header.closeHeaderA .. url .. header.closeHeaderB, header.rednetHeader..connection.channel)
								Modem.close(connection.channel)
								connection = nil
							end
						end
					})

					disconnectOthers(1)
					return directResults[1]
				end
			end
		elseif event == "rednet_message" then
			local fullMatch = responseMatch .. os.getComputerID() .. header.responseMatchC
			if protocol and tonumber(protocol:match(header.rednetMatch)) == serverChannel and channel:match(fullMatch) and type(textutils.unserialize(channel:match(fullMatch))) == "table" then
				local key = Handshake.generateResponseData(textutils.unserialize(channel:match(fullMatch)))
				if key then
					local connection = SecureConnection.new(key, url, url, id, true)
					table.insert(rednetResults, {
						connection = connection,
						fetchPage = function(page)
							if not connection then
								return nil
							end

							local fetchTimer = os.startTimer(fetchTimeout)

							local pageRequest = header.pageRequestHeaderA .. url .. header.pageRequestHeaderB .. page
							local pageResponseMatch = header.pageResponseMatchA .. Cryptography.sanatize(url) .. header.pageResponseMatchB

							connection:sendMessage(pageRequest, header.rednetHeader .. connection.channel)

							while true do
								local event, id, channel, protocol, message, dist = os.pullEventRaw()
								if event == "rednet_message" and protocol and tonumber(protocol:match(header.rednetMatch)) == connection.channel and connection:verifyHeader(channel) then
									local resp, data = connection:decryptMessage(channel)
									if not resp then
										-- Decryption error
									elseif data and data ~= page then
										if data:match(pageResponseMatch) then
											local head, body = data:match(pageResponseMatch)
											return body, textutils.unserialize(head)
										end
									end
								elseif event == "timer" and id == fetchTimer then
									return nil
								end
							end
						end,
						close = function()
							connection:sendMessage(header.closeHeaderA .. url .. header.closeHeaderB, header.rednetHeader..connection.channel)
							Modem.close(connection.channel)
							connection = nil
						end
					})

					if #rednetResults == 1 then
						timer = os.startTimer(0.2)
					end
				end
			end
		elseif event == "timer" and id == timer then
			-- Return
			if #directResults > 0 then
				disconnectOthers(1)
				return directResults[1]
			elseif #rednetResults > 0 then
				local lowestID = math.huge
				local lowestResult = nil
				for k,v in pairs(rednetResults) do
					if v.connection.rednet_id < lowestID then
						lowestID = v.connection.rednet_id
						lowestResult = v
					end
				end

				for k,v in pairs(rednetResults) do
					if v.connection.rednet_id ~= lowestID then
						v.close()
					end
				end

				return lowestResult
			else
				return nil
			end
		end
	end
end



--    Fetching Raw Data


local fetchSearchResultsForQuery = function(query)
	local all = protocols[currentProtocol]["fetchAllSearchResults"]()
	local results = {}
	if query and query:len() > 0 then
		for _, v in pairs(all) do
			if v:find(query:lower()) then
				table.insert(results, v)
			end
		end
	else
		results = all
	end

	table.sort(results)
	return results
end


local getConnectionObjectFromURL = function(url)
	local domain = url:match("^([^/]+)")
	return protocols[currentProtocol]["fetchConnectionObject"](domain)
end


local determineLanguage = function(header)
	if type(header) == "table" then
		if header.language and header.language == "Firewolf Markup" then
			return "fwml"
		else
			return "lua"
		end
	else
		return "lua"
	end
end



--    History


local appendToHistory = function(url)
	if history[1] ~= url then
		table.insert(history, 1, url)
	end
end



--    Fetch Websites


local loadingAnimation = function()
	local state = -2

	term.setTextColor(theme.text)
	term.setBackgroundColor(theme.accent)

	term.setCursorPos(w - 5, 1)
	term.write("[=  ]")

	local timer = os.startTimer(animationInterval)

	while true do
		local event, timerID = os.pullEvent()
		if event == "timer" and timerID == timer then
			term.setTextColor(theme.text)
			term.setBackgroundColor(theme.accent)

			state = state + 1

			term.setCursorPos(w - 5, 1)
			term.write("[   ]")
			term.setCursorPos(w - 2 - math.abs(state), 1)
			term.write("=")

			if state == 2 then
				state = -2
			end

			timer = os.startTimer(animationInterval)
		end
	end
end


local normalizeURL = function(url)
	url = url:lower():gsub(" ", "")
	if url == "home" or url == "homepage" then
		url = "firewolf"
	end

	return url
end


local normalizePage = function(page)
	if not page then page = "" end
	page = page:lower()
	if page == "" then
		page = "/"
	end
	return page
end


local determineActionForURL = function(url)
	if url:len() > 0 and url:gsub("/", ""):len() == 0 then
		return "none"
	end

	if url == "exit" then
		return "exit"
	elseif builtInSites["display"][url] then
		return "internal website"
	elseif url == "" then
		local results = fetchSearchResultsForQuery()
		if #results > 0 then
			return "search", results
		else
			return "none"
		end
	else
		local connection = getConnectionObjectFromURL(url)
		if connection then
			return "external website", connection
		else
			local results = fetchSearchResultsForQuery(url)
			if #results > 0 then
				return "search", results
			else
				return "none"
			end
		end
	end
end


local fetchSearch = function(url, results)
	return languages["lua"]["runWithoutAntivirus"](builtInSites["search"], results)
end


local fetchInternal = function(url)
	return languages["lua"]["runWithoutAntivirus"](builtInSites["display"][url])
end


local fetchError = function(err)
	return languages["lua"]["runWithoutAntivirus"](builtInSites["error"], err)
end


local fetchExternal = function(url, connection)
	if connection.multipleServers then
		-- Please forgive me
		-- GravityScore forced me to do it like this
		-- I don't mean it, I really don't.
		connection = connection.servers[1]
	end

	local page = normalizePage(url:match("^[^/]+/(.+)"))
	local contents, head = connection.fetchPage(page)
	if contents then
		if type(contents) ~= "string" then
			return fetchNone()
		else
			local language = determineLanguage(head)
			return languages[language]["run"](contents, page, connection)
		end
	else
		if connection then
			connection.close()
			return "retry"
		end
		return fetchError("A connection error/timeout has occurred!")
	end
end


local fetchNone = function()
	return languages["lua"]["runWithoutAntivirus"](builtInSites["noresults"])
end


local fetchURL = function(url, inheritConnection)
	url = normalizeURL(url)
	currentWebsiteURL = url

	if inheritConnection then
		local resp = fetchExternal(url, inheritConnection)
		if resp ~= "retry" then
			return resp, false, inheritConnection
		end
	end

	local action, connection = determineActionForURL(url)

	if action == "search" then
		return fetchSearch(url, connection), true
	elseif action == "internal website" then
		return fetchInternal(url), true
	elseif action == "external website" then
		local resp = fetchExternal(url, connection)
		if resp == "retry" then
			return fetchError("A connection error/timeout has occurred!"), false, connection
		else
			return resp, false, connection
		end
	elseif action == "none" then
		return fetchNone(), true
	elseif action == "exit" then
		os.queueEvent("terminate")
	end

	return nil
end



--    Tabs


local switchTab = function(index, shouldntResume)
	if not tabs[index] then
		return
	end

	if tabs[currentTab].win then
		tabs[currentTab].win.setVisible(false)
	end

	currentTab = index
	isMenubarOpen = tabs[currentTab].isMenubarOpen
	currentWebsiteURL = tabs[currentTab].url

	term.redirect(originalTerminal)
	clear(theme.background, theme.text)
	drawMenubar()

	term.redirect(tabs[currentTab].win)
	term.setCursorPos(1, 1)
	tabs[currentTab].win.setVisible(true)
	tabs[currentTab].win.redraw()

	if not shouldntResume then
		coroutine.resume(tabs[currentTab].thread)
	end
end


local closeCurrentTab = function()
	if #tabs <= 0 then
		return
	end

	table.remove(tabs, currentTab)

	currentTab = math.max(currentTab - 1, 1)
	switchTab(currentTab, true)
end


local loadTab = function(index, url, givenFunc)
	url = normalizeURL(url)

	local func = nil
	local isOpen = true
	local currentConnection = false

	isMenubarOpen = true
	currentWebsiteURL = url
	drawMenubar()

	if tabs[index] and tabs[index].connection and tabs[index].url then
		if url:match("^([^/]+)") == tabs[index].url:match("^([^/]+)") then
			currentConnection = tabs[index].connection
		else
			tabs[index].connection.close()
			tabs[index].connection = nil
		end
	end

	if givenFunc then
		func = givenFunc
	else
		parallel.waitForAny(function()
			func, isOpen, connection = fetchURL(url, currentConnection)
		end, function()
			while true do
				local event, key = os.pullEvent()
				if event == "key" and (key == keys.leftCtrl or key == keys.rightCtrl) then
					break
				end
			end
		end, loadingAnimation)
	end

	if func then
		appendToHistory(url)

		tabs[index] = {}
		tabs[index].url = url
		tabs[index].connection = connection
		tabs[index].win = window.create(originalTerminal, 1, 1, w, h, false)

		tabs[index].thread = coroutine.create(func)
		tabs[index].isMenubarOpen = isOpen
		tabs[index].isMenubarPermanent = isOpen

		tabs[index].ox = 1
		tabs[index].oy = 1

		term.redirect(tabs[index].win)
		clear(theme.background, theme.text)

		switchTab(index)
	end
end



--    Website Environments


local getWhitelistedEnvironment = function()
	local env = {}

	local function copy(source, destination, key)
		destination[key] = {}
		for k, v in pairs(source) do
			destination[key][k] = v
		end
	end

	copy(bit, env, "bit")
	copy(colors, env, "colors")
	copy(colours, env, "colours")
	copy(coroutine, env, "coroutine")

	copy(disk, env, "disk")
	env["disk"]["setLabel"] = nil
	env["disk"]["eject"] = nil

	copy(gps, env, "gps")
	copy(help, env, "help")
	copy(keys, env, "keys")
	copy(math, env, "math")

	copy(os, env, "os")
	env["os"]["run"] = nil
	env["os"]["shutdown"] = nil
	env["os"]["reboot"] = nil
	env["os"]["setComputerLabel"] = nil
	env["os"]["queueEvent"] = nil
	env["os"]["pullEvent"] = function(filter)
		while true do
			local event = {os.pullEvent(filter)}
			if not filter then
				return unpack(event)
			elseif filter and event[1] == filter then
				return unpack(event)
			end
		end
	end
	env["os"]["pullEventRaw"] = env["os"]["pullEvent"]

	copy(paintutils, env, "paintutils")
	copy(parallel, env, "parallel")
	copy(peripheral, env, "peripheral")
	copy(rednet, env, "rednet")
	copy(redstone, env, "redstone")
	copy(redstone, env, "rs")

	copy(shell, env, "shell")
	env["shell"]["run"] = nil
	env["shell"]["exit"] = nil
	env["shell"]["setDir"] = nil
	env["shell"]["setAlias"] = nil
	env["shell"]["clearAlias"] = nil
	env["shell"]["setPath"] = nil
	env["shell"]["openTab"] = nil

	copy(string, env, "string")
	copy(table, env, "table")

	copy(term, env, "term")
	env["term"]["redirect"] = nil
	env["term"]["restore"] = nil

	copy(textutils, env, "textutils")
	copy(vector, env, "vector")

	if turtle then
		copy(turtle, env, "turtle")
	end

	if http then
		copy(http, env, "http")
	end

	env["assert"] = assert
	env["printError"] = printError
	env["tonumber"] = tonumber
	env["tostring"] = tostring
	env["type"] = type
	env["next"] = next
	env["unpack"] = unpack
	env["pcall"] = pcall
	env["xpcall"] = xpcall
	env["sleep"] = sleep
	env["pairs"] = pairs
	env["ipairs"] = ipairs
	env["read"] = read
	env["write"] = write
	env["select"] = select
	env["print"] = print
	env["setmetatable"] = setmetatable
	env["getmetatable"] = getmetatable

	env["_G"] = env

	return env
end


local overrideEnvironment = function(env)
	local localTerm = {}
	for k, v in pairs(term) do
		localTerm[k] = v
	end

	env["term"]["clear"] = function()
		localTerm.clear()
		drawMenubar()
	end

	env["term"]["scroll"] = function(n)
		localTerm.scroll(n)
		drawMenubar()
	end

	env["shell"]["getRunningProgram"] = function()
		return currentWebsiteURL
	end
end

local urlEncode = function(url)
	local result = url

	result = result:gsub("%%", "%%a")
	result = result:gsub(":", "%%c")
	result = result:gsub("/", "%%s")
	result = result:gsub("\n", "%%n")
	result = result:gsub(" ", "%%w")
	result = result:gsub("&", "%%m")
	result = result:gsub("%?", "%%q")
	result = result:gsub("=", "%%e")
	result = result:gsub("%.", "%%d")

	return result
end

local urlDecode = function(url)
	local result = url

	result = result:gsub("%%c", ":")
	result = result:gsub("%%s", "/")
	result = result:gsub("%%n", "\n")
	result = result:gsub("%%w", " ")
	result = result:gsub("%%&", "&")
	result = result:gsub("%%q", "%?")
	result = result:gsub("%%e", "=")
	result = result:gsub("%%d", "%.")
	result = result:gsub("%%m", "%%")

	return result
end

local applyAPIFunctions = function(env, connection)
	env["firewolf"] = {}
	env["firewolf"]["version"] = version
	env["firewolf"]["domain"] = currentWebsiteURL:match("^[^/]+")

	env["firewolf"]["redirect"] = function(url)
		if type(url) ~= "string" then
			return error("string (url) expected, got " .. type(url))
		end

		os.queueEvent(redirectEvent, url)
		coroutine.yield()
	end

	env["firewolf"]["download"] = function(page)
		if type(page) ~= "string" then
			return error("string (page) expected")
		end
		local bannedNames = {"ls", "dir", "delete", "copy", "move", "list", "rm", "cp", "mv", "clear", "cd", "lua"}

		local startSearch, endSearch = page:find(currentWebsiteURL:match("^[^/]+"))
		if startSearch == 1 then
			if page:sub(endSearch + 1, endSearch + 1) == "/" then
				page = page:sub(endSearch + 2, -1)
			else
				page = page:sub(endSearch + 1, -1)
			end
		end

		local filename = page:match("([^/]+)$")
		if not filename then
			return false, "Cannot download index"
		end

		for k, v in pairs(bannedNames) do
			if filename == v then
				return false, "Filename prohibited!"
			end
		end

		if not fs.exists(downloadsLocation) then
			fs.makeDir(downloadsLocation)
		elseif not fs.isDir(downloadsLocation) then
			return false, "Downloads disabled!"
		end

		contents = connection.fetchPage(normalizePage(page))
		if type(contents) ~= "string" then
			return false, "Download error!"
		else
			local f = io.open(downloadsLocation .. "/" .. filename, "w")
			f:write(contents)
			f:close()
			return true, downloadsLocation .. "/" .. filename
		end
	end

	env["firewolf"]["encode"] = function(vars)
		if type(vars) ~= "table" then
			return error("table (vars) expected, got " .. type(vars))
		end

		local startSearch, endSearch = page:find(currentWebsiteURL:match("^[^/]+"))
		if startSearch == 1 then
			if page:sub(endSearch + 1, endSearch + 1) == "/" then
				page = page:sub(endSearch + 2, -1)
			else
				page = page:sub(endSearch + 1, -1)
			end
		end

		local construct = "?"
		for k,v in pairs(vars) do
 			construct = construct .. urlEncode(tostring(k)) .. "=" .. urlEncode(tostring(v)) .. "&"
		end
		-- Get rid of that last ampersand
		construct = construct:sub(1, -2)

		return construct
	end

	env["firewolf"]["query"] = function(page, vars)
		if type(page) ~= "string" then
			return error("string (page) expected, got " .. type(page))
		end
		if vars and type(vars) ~= "table" then
			return error("table (vars) expected, got " .. type(vars))
		end

		local startSearch, endSearch = page:find(currentWebsiteURL:match("^[^/]+"))
		if startSearch == 1 then
			if page:sub(endSearch + 1, endSearch + 1) == "/" then
				page = page:sub(endSearch + 2, -1)
			else
				page = page:sub(endSearch + 1, -1)
			end
		end

		local construct = page .. "?"
		if vars then
			for k,v in pairs(vars) do
	 			construct = construct .. urlEncode(tostring(k)) .. "=" .. urlEncode(tostring(v)) .. "&"
			end
		end
		-- Get rid of that last ampersand
		construct = construct:sub(1, -2)

		contents = connection.fetchPage(normalizePage(construct))
		if type(contents) == "string" then
			return contents
		else
			return false
		end
	end

	env["firewolf"]["loadImage"] = function(page)
		if type(page) ~= "string" then
			return error("string (page) expected, got " .. type(page))
		end

		local startSearch, endSearch = page:find(currentWebsiteURL:match("^[^/]+"))
		if startSearch == 1 then
			if page:sub(endSearch + 1, endSearch + 1) == "/" then
				page = page:sub(endSearch + 2, -1)
			else
				page = page:sub(endSearch + 1, -1)
			end
		end

		local filename = page:match("([^/]+)$")
		if not filename then
			return false, "Cannot load index as an image!"
		end

		contents = connection.fetchPage(normalizePage(page))
		if type(contents) ~= "string" then
			return false, "Download error!"
		else
			local colorLookup = {}
			for n = 1, 16 do
				colorLookup[string.byte("0123456789abcdef", n, n)] = 2 ^ (n - 1)
			end

			local image = {}
			for line in contents:gmatch("[^\n]+") do
				local lines = {}
				for x = 1, line:len() do
					lines[x] = colorLookup[string.byte(line, x, x)] or 0
				end
				table.insert(image, lines)
			end

			return image
		end
	end

	env["center"] = center
	env["fill"] = fill
end


local getWebsiteEnvironment = function(antivirus, connection)
	local env = {}

	if antivirus then
		env = getWhitelistedEnvironment()
		overrideEnvironment(env)
	else
		setmetatable(env, {__index = _G})
	end

	applyAPIFunctions(env, connection)

	return env
end



--    FWML Execution


local render = {}

render["functions"] = {}
render["functions"]["public"] = {}
render["alignations"] = {}

render["variables"] = {
	scroll,
	maxScroll,
	align,
	linkData = {},
	blockLength,
	link,
	linkStart,
	markers,
	currentOffset,
}


local function getLine(loc, data)
	local _, changes = data:sub(1, loc):gsub("\n", "")
	if not changes then
		return 1
	else
		return changes + 1
	end
end


local function parseData(data)
	local commands = {}
	local searchPos = 1

	while #data > 0 do
		local sCmd, eCmd = data:find("%[[^%]]+%]", searchPos)
		if sCmd then
			sCmd = sCmd + 1
			eCmd = eCmd - 1

			if (sCmd > 2) then
				if data:sub(sCmd - 2, sCmd - 2) == "\\" then
					local t = data:sub(searchPos, sCmd - 1):gsub("\n", ""):gsub("\\%[", "%["):gsub("\\%]", "%]")
					if #t > 0 then
						if #commands > 0 and type(commands[#commands][1]) == "string" then
							commands[#commands][1] = commands[#commands][1] .. t
						else
							table.insert(commands, {t})
						end
					end
					searchPos = sCmd
				else
					local t = data:sub(searchPos, sCmd - 2):gsub("\n", ""):gsub("\\%[", "%["):gsub("\\%]", "%]")
					if #t > 0 then
						if #commands > 0 and type(commands[#commands][1]) == "string" then
							commands[#commands][1] = commands[#commands][1] .. t
						else
							table.insert(commands, {t})
						end
					end

					t = data:sub(sCmd, eCmd):gsub("\n", "")
					table.insert(commands, {getLine(sCmd, data), t})
					searchPos = eCmd + 2
				end
			else
				local t = data:sub(sCmd, eCmd):gsub("\n", "")
				table.insert(commands, {getLine(sCmd, data), t})
				searchPos = eCmd + 2
			end
		else
			local t = data:sub(searchPos, -1):gsub("\n", ""):gsub("\\%[", "%["):gsub("\\%]", "%]")
			if #t > 0 then
				if #commands > 0 and type(commands[#commands][1]) == "string" then
					commands[#commands][1] = commands[#commands][1] .. t
				else
					table.insert(commands, {t})
				end
			end

			break
		end
	end

	return commands
end


local function proccessData(commands)
	searchIndex = 0

	while searchIndex < #commands do
		searchIndex = searchIndex + 1

		local length = 0
		local origin = searchIndex

		if type(commands[searchIndex][1]) == "string" then
			length = length + #commands[searchIndex][1]
			local endIndex = origin
			for i = origin + 1, #commands do
				if commands[i][2] then
					local command = commands[i][2]:match("^(%w+)%s-")
					if not (command == "c" or command == "color" or command == "bg"
							or command == "background" or command == "newlink" or command == "endlink") then
						endIndex = i
						break
					end
				elseif commands[i][2] then

				else
					length = length + #commands[i][1]
				end
				if i == #commands then
					endIndex = i
				end
			end

			commands[origin][2] = length
			searchIndex = endIndex
			length = 0
		end
	end

	return commands
end


local function parse(original)
	return proccessData(parseData(original))
end


render["functions"]["display"] = function(text, length, offset, center)
	if not offset then
		offset = 0
	end

	return render.variables.align(text, length, w, offset, center);
end


render["functions"]["displayText"] = function(source)
	if source[2] then
		render.variables.blockLength = source[2]
		if render.variables.link and not render.variables.linkStart then
			render.variables.linkStart = render.functions.display(
				source[1], render.variables.blockLength, render.variables.currentOffset, w / 2)
		else
			render.functions.display(source[1], render.variables.blockLength, render.variables.currentOffset, w / 2)
		end
	else
		if render.variables.link and not render.variables.linkStart then
			render.variables.linkStart = render.functions.display(source[1], nil, render.variables.currentOffset, w / 2)
		else
			render.functions.display(source[1], nil, render.variables.currentOffset, w / 2)
		end
	end
end


render["functions"]["public"]["br"] = function(source)
	if render.variables.link then
		return "Cannot insert new line within a link on line " .. source[1]
	end

	render.variables.scroll = render.variables.scroll + 1
	render.variables.maxScroll = math.max(render.variables.scroll, render.variables.maxScroll)
end


render["functions"]["public"]["c "] = function(source)
	local sColor = source[2]:match("^%w+%s+(.+)$") or ""
	if colors[sColor] then
		term.setTextColor(colors[sColor])
	else
		return "Invalid color: \"" .. sColor .. "\" on line " .. source[1]
	end
end


render["functions"]["public"]["color "] = render["functions"]["public"]["c "]


render["functions"]["public"]["bg "] = function(source)
	local sColor = source[2]:match("^%w+%s+(.+)$") or ""
	if colors[sColor] then
		term.setBackgroundColor(colors[sColor])
	else
		return "Invalid color: \"" .. sColor .. "\" on line " .. source[1]
	end
end


render["functions"]["public"]["background "] = render["functions"]["public"]["bg "]


render["functions"]["public"]["newlink "] = function(source)
	if render.variables.link then
		return "Cannot nest links on line " .. source[1]
	end

	render.variables.link = source[2]:match("^%w+%s+(.+)$") or ""
	render.variables.linkStart = false
end


render["functions"]["public"]["endlink"] = function(source)
	if not render.variables.link then
		return "Cannot end a link without a link on line " .. source[1]
	end

	local linkEnd = term.getCursorPos()-1
	table.insert(render.variables.linkData, {render.variables.linkStart,
		linkEnd, render.variables.scroll, render.variables.link})
	render.variables.link = false
	render.variables.linkStart = false
end


render["functions"]["public"]["offset "] = function(source)
	local offset = tonumber((source[2]:match("^%w+%s+(.+)$") or ""))
	if offset then
		render.variables.currentOffset = offset
	else
		return "Invalid offset value: \"" .. (source[2]:match("^%w+%s+(.+)$") or "") .. "\" on line " .. source[1]
	end
end


render["functions"]["public"]["marker "] = function(source)
	render.variables.markers[(source[2]:match("^%w+%s+(.+)$") or "")] = render.variables.scroll
end


render["functions"]["public"]["goto "] = function(source)
	local location = source[2]:match("%w+%s+(.+)$")
	if render.variables.markers[location] then
		render.variables.scroll = render.variables.markers[location]
	else
		return "No such location: \"" .. (source[2]:match("%w+%s+(.+)$") or "") .. "\" on line " .. source[1]
	end
end


render["functions"]["public"]["box "] = function(source)
	local sColor, align, height, width, offset, url = source[2]:match("^box (%a+) (%a+) (%-?%d+) (%-?%d+) (%-?%d+) ?([^ ]*)")
	if not sColor then
		return "Invalid box syntax on line " .. source[1]
	end

	local x, y = term.getCursorPos()
	local startX

	if align == "center" or align == "centre" then
		startX = math.ceil((w / 2) - width / 2) + offset
	elseif align == "left" then
		startX = 1 + offset
	elseif align == "right" then
		startX = (w - width + 1) + offset
	else
		return "Invalid align option for box on line " .. source[1]
	end

	if not colors[sColor] then
		return "Invalid color: \"" .. sColor .. "\" for box on line " .. source[1]
	end

	term.setBackgroundColor(colors[sColor])
	for i = 0, height - 1 do
		term.setCursorPos(startX, render.variables.scroll + i)
		term.write(string.rep(" ", width))
		if url:len() > 3 then
			table.insert(render.variables.linkData, {startX, startX + width - 1, render.variables.scroll + i, url})
		end
	end

	render.variables.maxScroll = math.max(render.variables.scroll + height - 1, render.variables.maxScroll)
	term.setCursorPos(x, y)
end


render["alignations"]["left"] = function(text, length, _, offset)
	local x, y = term.getCursorPos()
	if length then
		term.setCursorPos(1 + offset, render.variables.scroll)
		term.write(text)
		return 1 + offset
	else
		term.setCursorPos(x, render.variables.scroll)
		term.write(text)
		return x
	end
end


render["alignations"]["right"] = function(text, length, width, offset)
	local x, y = term.getCursorPos()
	if length then
		term.setCursorPos((width - length + 1) + offset, render.variables.scroll)
		term.write(text)
		return (width - length + 1) + offset
	else
		term.setCursorPos(x, render.variables.scroll)
		term.write(text)
		return x
	end
end


render["alignations"]["center"] = function(text, length, _, offset, center)
	local x, y = term.getCursorPos()
	if length then
		term.setCursorPos(math.ceil(center - length / 2) + offset, render.variables.scroll)
		term.write(text)
		return math.ceil(center - length / 2) + offset
	else
		term.setCursorPos(x, render.variables.scroll)
		term.write(text)
		return x
	end
end


render["render"] = function(data, startScroll)
	if startScroll == nil then
		render.variables.startScroll = 0
	else
		render.variables.startScroll = startScroll
	end

	render.variables.scroll = startScroll + 1
	render.variables.maxScroll = render.variables.scroll

	render.variables.linkData = {}

	render.variables.align = render.alignations.left

	render.variables.blockLength = 0
	render.variables.link = false
	render.variables.linkStart = false
	render.variables.markers = {}
	render.variables.currentOffset = 0

	for k, v in pairs(data) do
		if type(v[2]) ~= "string" then
			render.functions.displayText(v)
		elseif v[2] == "<" or v[2] == "left" then
			render.variables.align = render.alignations.left
		elseif v[2] == ">" or v[2] == "right" then
			render.variables.align = render.alignations.right
		elseif v[2] == "=" or v[2] == "center" then
			render.variables.align = render.alignations.center
		else
			local existentFunction = false

			for name, func in pairs(render.functions.public) do
				if v[2]:find(name) == 1 then
					existentFunction = true
					local ret = func(v)
					if ret then
						return ret
					end
				end
			end

			if not existentFunction then
				return "Non-existent tag: \"" .. v[2] .. "\" on line " .. v[1]
			end
		end
	end

	return render.variables.linkData, render.variables.maxScroll - render.variables.startScroll
end



--    Lua Execution


languages["lua"] = {}
languages["fwml"] = {}


languages["lua"]["runWithErrorCatching"] = function(func, ...)
	local _, err = pcall(func, ...)
	if err then
		os.queueEvent(websiteErrorEvent, err)
	end
end


languages["lua"]["runWithoutAntivirus"] = function(func, ...)
	local args = {...}
	local env = getWebsiteEnvironment(false)
	setfenv(func, env)
	return function()
		languages["lua"]["runWithErrorCatching"](func, unpack(args))
	end
end


languages["lua"]["run"] = function(contents, page, connection, ...)
	local func, err = loadstring("sleep(0) " .. contents, page)
	if err then
		return languages["lua"]["runWithoutAntivirus"](builtInSites["crash"], err)
	else
		local args = {...}
		local env = getWebsiteEnvironment(true, connection)
		setfenv(func, env)
		return function()
			languages["lua"]["runWithErrorCatching"](func, unpack(args))
		end
	end
end


languages["fwml"]["run"] = function(contents, page, connection, ...)
	local err, data = pcall(parse, contents)
	if not err then
		return languages["lua"]["runWithoutAntivirus"](builtInSites["crash"], data)
	end

	return function()
		local currentScroll = 0
		local err, links, pageHeight = pcall(render.render, data, currentScroll)
		if type(links) == "string" or not err then
			term.clear()
			os.queueEvent(websiteErrorEvent, links)
		else
			while true do
				local e, scroll, x, y = os.pullEvent()
				if e == "mouse_click" then
					for k, v in pairs(links) do
						if x >= math.min(v[1], v[2]) and x <= math.max(v[1], v[2]) and y == v[3] then
							os.queueEvent(redirectEvent, v[4])
							coroutine.yield()
						end
					end
				elseif e == "mouse_scroll" then
					if currentScroll - scroll - h >= -pageHeight and currentScroll - scroll <= 0 then
						currentScroll = currentScroll - scroll
						clear(theme.background, theme.text)
						links = render.render(data, currentScroll)
					end
				elseif e == "key" and scroll == keys.up or scroll == keys.down then
					local scrollAmount

					if scroll == keys.up then
						scrollAmount = 1
					elseif scroll == keys.down then
						scrollAmount = -1
					end

					local scrollLessHeight = currentScroll + scrollAmount - h >= -pageHeight
					local scrollZero = currentScroll + scrollAmount <= 0
					if scrollLessHeight and scrollZero then
						currentScroll = currentScroll + scrollAmount
						clear(theme.background, theme.text)
						links = render.render(data, currentScroll)
					end
				end
			end
		end
	end
end



--    Query Bar


local readNewWebsiteURL = function()
	local onEvent = function(text, event, key, x, y)
		if event == "mouse_click" then
			if y == 2 then
				local index = determineClickedTab(x, y)
				if index == "new" and #tabs < maxTabs then
					loadTab(#tabs + 1, "firewolf")
				elseif index == "close" then
					closeCurrentTab()
				elseif index then
					switchTab(index)
				end

				return {["nullifyText"] = true, ["exit"] = true}
			elseif y > 2 then
				return {["nullifyText"] = true, ["exit"] = true}
			end
		elseif event == "key" then
			if key == keys.leftCtrl or key == keys.rightCtrl then
				return {["nullifyText"] = true, ["exit"] = true}
			end
		end
	end

	isMenubarOpen = true
	drawMenubar()
	term.setCursorPos(2, 1)
	term.setTextColor(theme.text)
	term.setBackgroundColor(theme.accent)
	term.clearLine()
	term.write(currentProtocol .. "://")

	local website = modifiedRead({
		["onEvent"] = onEvent,
		["displayLength"] = w - 9,
		["history"] = history,
	})

	if not website then
		if not tabs[currentTab].isMenubarPermanent then
			isMenubarOpen = false
			menubarWindow.setVisible(false)
		else
			isMenubarOpen = true
			menubarWindow.setVisible(true)
		end

		term.redirect(tabs[currentTab].win)
		tabs[currentTab].win.setVisible(true)
		tabs[currentTab].win.redraw()

		return
	elseif website == "exit" then
		error()
	end

	loadTab(currentTab, website)
end



--    Event Management


local handleKeyDown = function(event)
	if event[2] == keys.leftCtrl or event[2] == keys.rightCtrl then
		readNewWebsiteURL()
		return true
	end

	return false
end


local handleMouseDown = function(event)
	if isMenubarOpen then
		if event[4] == 1 then
			readNewWebsiteURL()
			return true
		elseif event[4] == 2 then
			local index = determineClickedTab(event[3], event[4])
			if index == "new" and #tabs < maxTabs then
				loadTab(#tabs + 1, "firewolf")
			elseif index == "close" then
				closeCurrentTab()
			elseif index then
				switchTab(index)
			end

			return true
		end
	end

	return false
end


local handleEvents = function()
	loadTab(1, "firewolf")
	currentTab = 1

	while true do
		drawMenubar()
		local event = {os.pullEventRaw()}
		drawMenubar()

		local cancelEvent = false
		if event[1] == "terminate" then
			break
		elseif event[1] == "key" then
			cancelEvent = handleKeyDown(event)
		elseif event[1] == "mouse_click" then
			cancelEvent = handleMouseDown(event)
		elseif event[1] == websiteErrorEvent then
			cancelEvent = true

			loadTab(currentTab, tabs[currentTab].url, function()
				builtInSites["crash"](event[2])
			end)
		elseif event[1] == redirectEvent then
			cancelEvent = true

			if (event[2]:match("^rdnt://(.+)$")) then
				event[2] = event[2]:match("^rdnt://(.+)$")
			end

			loadTab(currentTab, event[2])
		end

		if not cancelEvent then
			term.redirect(tabs[currentTab].win)
			term.setCursorPos(tabs[currentTab].ox, tabs[currentTab].oy)

			coroutine.resume(tabs[currentTab].thread, unpack(event))

			local ox, oy = term.getCursorPos()
			tabs[currentTab].ox = ox
			tabs[currentTab].oy = oy
		end
	end
end



--    Main


local main = function()
	currentProtocol = "rdnt"
	currentTab = 1

	if term.isColor() then
		theme = colorTheme
		enableTabBar = true
	else
		theme = grayscaleTheme
		enableTabBar = false
	end

	setupMenubar()
	protocols[currentProtocol]["setup"]()

	clear(theme.background, theme.text)
	handleEvents()
end


local handleError = function(err)
	clear(theme.background, theme.text)

	fill(1, 3, w, 3, theme.subtle)
	term.setCursorPos(1, 4)
	center("Firewolf has crashed!")

	term.setBackgroundColor(theme.background)
	term.setCursorPos(1, 8)
	centerSplit(err, w - 4)
	print("\n")
	center("Please report this error to")
	center("GravityScore or 1lann.")
	print("")
	center("Press any key to exit.")

	os.pullEvent("key")
	os.queueEvent("")
	os.pullEvent()
end

local _, err = pcall(main)
term.redirect(originalTerminal)

Modem.closeAll()

if err and not err:lower():find("terminate") then
	handleError(err)
end


clear(colors.black, colors.white)
center("Thanks for using Firewolf " .. version)
center("Made by GravityScore and 1lann")
print("")