local Terminal = require(script.luau_term)
local TERMINAL_WIDTH = 80
local TERMINAL_HEIGHT = 30
local TERMINAL_CANVAS_TYPE: "Bitmap" = "Bitmap"

local std = script:WaitForChild("stdio") :: RemoteEvent
local newSession = script:WaitForChild("NewSession") :: RemoteFunction

local parsedFont
if TERMINAL_CANVAS_TYPE == "Bitmap" then
	local font = (script:WaitForChild("GetFont") :: RemoteFunction):InvokeServer()
	local parseUnifont = Terminal.parseUnifont
	parsedFont = parseUnifont(font)
end

local term = Terminal.createTerminal(TERMINAL_CANVAS_TYPE, TERMINAL_WIDTH, TERMINAL_HEIGHT, parsedFont)

local sessionid = newSession:InvokeServer(term.currentScreen.canvas.width, term.currentScreen.canvas.height, "xterm-256color")
function term:stdin(str)
	std:FireServer(sessionid, str)
end

local pixelSize = Vector2.new(term.currentScreen.canvas.width * 8, term.currentScreen.canvas.height * 16)

local player = game:GetService("Players").LocalPlayer
local PlayerGui = player:WaitForChild("PlayerGui") :: PlayerGui

local gui = Instance.new("ScreenGui")
gui.ResetOnSpawn = false
gui.Parent = PlayerGui

local container = Instance.new("Frame")
container.AnchorPoint = Vector2.new(0.5, 0.5)
container.Size = UDim2.fromOffset(pixelSize.X, pixelSize.Y)
container.Position = UDim2.fromScale(0.5, 0.5)
container.Parent = gui

local _renderCanvas

if TERMINAL_CANVAS_TYPE == "Bitmap" then
	local label = Instance.new("ImageLabel")
	label.Size = UDim2.fromScale(1, 1)
	label.ResampleMode = Enum.ResamplerMode.Pixelated
	label.Parent = container

	local image = game:GetService("AssetService"):CreateEditableImage({
		Size = pixelSize,
	})

	label.ImageContent = Content.fromObject(image)

	_renderCanvas = function()
		image:WritePixelsBuffer(Vector2.zero, pixelSize, term.currentScreen.canvas.canvas)
	end
elseif TERMINAL_CANVAS_TYPE == "TextLabel" then
	local normalScreen = term.screens.normal
	local normalScreenFrame = normalScreen.canvas.canvas.frame
	local alternateScreen = term.screens.alternate
	local alternateScreenFrame = alternateScreen.canvas.canvas.frame

	normalScreenFrame.Parent = container
	alternateScreenFrame.Parent = container

	_renderCanvas = function()
		local currentScreen = term.currentScreen
		if currentScreen == term.screens.normal then
			normalScreenFrame.Visible = true
			alternateScreenFrame.Visible = false
		else
			normalScreenFrame.Visible = false
			alternateScreenFrame.Visible = true
		end
	end
end

local cursor = Instance.new("Frame")
cursor.Size = UDim2.fromOffset(6, 14)
cursor.BackgroundTransparency = 1
cursor.Parent = container
local stroke = Instance.new("UIStroke")
stroke.Thickness = 1
stroke.Color = Color3.new(1, 1, 1)
stroke.Parent = cursor

local function render()
	_renderCanvas()
	cursor.Position = UDim2.fromOffset(term.currentScreen.cursor.x * 8 + 1, term.currentScreen.cursor.y * 16 + 1)
end

render()

std.OnClientEvent:Connect(function(id, str)
	Terminal.stdout(term, buffer.fromstring(str))
	render()
end)

local createInput = require(script.input)
local info = createInput()
local inputBox = info.box

inputBox.ZIndex = 999
inputBox.Size = UDim2.fromScale(1, 1)
inputBox.Transparency = 1
inputBox.TextTransparency = 1
inputBox.TextSize = 0
inputBox.Parent = container

function term:changeCursorKeyFormat(mode)
	info.mode = mode
end

function term:changeCursorVisibility(visible)
	cursor.Visible = visible
end

function term:changeBracketedPasteMode(enable)
	info.bracketedPaste = enable
end

info.event.Event:Connect(function(str)
	std:FireServer(sessionid, str)
end)


-- Input handler, works by capturing inputs while the TextBox is focused
-- It saves the last pressed key, then uses it when Enum.UserInputType.TextInput is emitted
-- Using a TextBox allows us to capture all the keys that would otherwise be
-- captured by CoreGui and ignored by ContextActionService, like slash or function keys,
-- and when using TextInput if a key is held down, it will repeat

local UIS = game:GetService("UserInputService")

local inputBoxes: { [TextBox]: { box: TextBox, event: BindableEvent, mode: boolean, bracketedPaste: boolean } } = {}

local InputMap: { [number]: { [number]: number | string } } = {
	-- No modifier keys
	[0b00] = {},

	-- Only shift
	[0b01] = {},

	-- Only ctrl
	[0b10] = {},

	-- Both shift and ctrl
	[0b11] = {},
}

-- Generating the input map based on US keyboard layout:
do
	-- A-Z keycodes:
	-- Shift sets 6th bit to 0
	-- Ctrl sets 6th and 7th bits to 0
	-- Example: a = 0b1100001
	--          A = 0b1000001
	for code = Enum.KeyCode.A.Value, Enum.KeyCode.Z.Value do
		InputMap[0b00][code] = code
		InputMap[0b01][code] = bit32.band(code, 0b1011111)
		InputMap[0b10][code] = bit32.band(code, 0b0011111)
	end

	-- Rest of the regular keys, hardcoded to not do crazy math
	local lookup = "`1234567890-=[];'\\,./"
	local lookupShift = '~!@#$%^&*()_+{}:"|<>?'
	for i = 1, #lookup do
		local code = string.byte(lookup, i, i)
		InputMap[0b00][code] = code
		InputMap[0b01][code] = string.byte(lookupShift, i, i)
		InputMap[0b10][code] = string.byte(lookupShift, i, i)
	end

	-- Function keys
	InputMap[0b00][Enum.KeyCode.F1.Value] = "\x1bOP"
	InputMap[0b00][Enum.KeyCode.F2.Value] = "\x1bOQ"
	InputMap[0b00][Enum.KeyCode.F3.Value] = "\x1bOR"
	InputMap[0b00][Enum.KeyCode.F4.Value] = "\x1bOS"
	InputMap[0b00][Enum.KeyCode.F5.Value] = "\x1b[15~"
	InputMap[0b00][Enum.KeyCode.F6.Value] = "\x1b[17~"
	InputMap[0b00][Enum.KeyCode.F7.Value] = "\x1b[18~"
	InputMap[0b00][Enum.KeyCode.F8.Value] = "\x1b[19~"
	InputMap[0b00][Enum.KeyCode.F9.Value] = "\x1b[20~"
	InputMap[0b00][Enum.KeyCode.F10.Value] = "\x1b[21~"
	InputMap[0b00][Enum.KeyCode.F11.Value] = "\x1b[23~"
	InputMap[0b00][Enum.KeyCode.F12.Value] = "\x1b[24~"

	-- Special keys
	InputMap[0b00][Enum.KeyCode.Backspace.Value] = 0x7f
	InputMap[0b10][Enum.KeyCode.Backspace.Value] = 0x08
	InputMap[0b00][Enum.KeyCode.Tab.Value] = 0x09
	InputMap[0b00][Enum.KeyCode.Return.Value] = 0x0D
	InputMap[0b00][Enum.KeyCode.Space.Value] = 0x20

	table.freeze(InputMap[0b00])
	table.freeze(InputMap[0b01])
	table.freeze(InputMap[0b10])
	table.freeze(InputMap[0b11])

	table.freeze(InputMap)
end

-- Keys below don't trigger TextInput
local keysThatDontRepeat = {
	[Enum.KeyCode.Backspace.Value] = true,
	[Enum.KeyCode.Left.Value] = true,
	[Enum.KeyCode.Right.Value] = true,
	[Enum.KeyCode.Up.Value] = true,
	[Enum.KeyCode.Down.Value] = true,
	[Enum.KeyCode.F1.Value] = true,
	[Enum.KeyCode.F2.Value] = true,
	[Enum.KeyCode.F3.Value] = true,
	[Enum.KeyCode.F4.Value] = true,
	[Enum.KeyCode.F5.Value] = true,
	[Enum.KeyCode.F6.Value] = true,
	[Enum.KeyCode.F7.Value] = true,
	[Enum.KeyCode.F8.Value] = true,
	[Enum.KeyCode.F9.Value] = true,
	[Enum.KeyCode.F10.Value] = true,
	[Enum.KeyCode.F11.Value] = true,
	[Enum.KeyCode.F12.Value] = true,
}
local lastKeyCode: Enum.KeyCode = Enum.KeyCode.Unknown

local function handleInput(io: InputObject, handledByEngine: boolean)
	-- We expect to be focused in a TextBox when handling input
	if not handledByEngine then
		return
	end

	local focusedBox = UIS:GetFocusedTextBox()
	local inputInfo = focusedBox and inputBoxes[focusedBox] or nil
	if not inputInfo then
		return
	end
	local event = inputInfo.event

	local ctrl = io:IsModifierKeyDown(Enum.ModifierKey.Ctrl)
	if
		-- Check for keys that don't emit TextInput
		(keysThatDontRepeat[io.KeyCode.Value] and io.UserInputState == Enum.UserInputState.Begin)
		or (
			ctrl -- Edge case for when holding Ctrl, since it doesn't emit TextInput as well
			and io.UserInputState == Enum.UserInputState.Begin
			and io.UserInputType == Enum.UserInputType.Keyboard
			and io.KeyCode ~= Enum.KeyCode.Unknown
			-- Except Ctrl + V, which does emit TextInput
			and io.KeyCode ~= Enum.KeyCode.V
		)
	then
		lastKeyCode = io.KeyCode
		focusedBox.Text = ""
	else
		if
			io.UserInputState == Enum.UserInputState.Begin
			and io.UserInputType == Enum.UserInputType.Keyboard
			and io.KeyCode ~= Enum.KeyCode.Unknown
		then
			-- Save key to use when TextInput is emitted
			lastKeyCode = io.KeyCode
			return
		elseif
			io.UserInputState ~= Enum.UserInputState.Change
			or io.UserInputType ~= Enum.UserInputType.TextInput
			or not lastKeyCode
		then
			-- Skip anything that is not TextInput
			return
		end
	end

	if ctrl and lastKeyCode == Enum.KeyCode.V then
		task.delay(0, function()
			local paste = focusedBox.Text
			if inputInfo.bracketedPaste then
				paste = "\x1b[200~" .. paste .. "\x1b[201~"
			end
			event:Fire(paste)
			focusedBox.Text = ""
		end)
		return
	end

	local shift = io:IsModifierKeyDown(Enum.ModifierKey.Shift)
	local alt = io:IsModifierKeyDown(Enum.ModifierKey.Alt)

	local code = lastKeyCode.Value

	-- Special case for arrow keys
	if code > 0b100010000 and code < 0b100010101 then
		local mode = inputInfo.mode
		local shiftMod = shift and 0b100 or 0
		local ctrlMod = ctrl and 0b010 or 0
		local altMod = alt and 0b001 or 0
		local t = { "", "1;3", "1;5", "1;7", "", "1;4", "1;6", "1;8" }
		local mod = shiftMod + ctrlMod + altMod
		local str = string.format(
			"\27%s%s%s",
			(mode and mod == 0) and "O" or "[",
			t[mod + 1],
			string.char(bit32.band(code, 0b111) + 0b1000000)
		)
		event:Fire(str)
		return
	end

	local shiftMod = shift and 0b01 or 0
	local ctrlMod = ctrl and 0b10 or 0
	local charcode = InputMap[shiftMod + ctrlMod][code]
	if charcode then
		event:Fire(type(charcode) == "number" and utf8.char(charcode) or charcode)
	end
end

UIS.InputBegan:Connect(handleInput)
UIS.InputChanged:Connect(handleInput)

return function()
	local box = Instance.new("TextBox")
	box.Name = "InputBox"
	box.MultiLine = true
	box.ClearTextOnFocus = true
	local event = Instance.new("BindableEvent")
	event.Name = "InputEvent"
	event.Parent = box

	local info = {
		box = box,
		event = event,
		mode = false,
		bracketedPaste = false,
	}
	inputBoxes[box] = info

	box.FocusLost:Connect(function(enterPressed)
		if enterPressed then
			box:CaptureFocus()
		end
	end)

	return info
end


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

local Types = require("./types")

type BitmapCanvas = buffer

local BitmapCanvas = {} :: Types.CanvasModule<BitmapCanvas, buffer>

local underlineGlyph = buffer.create(16)
buffer.writeu8(underlineGlyph, 12, 0xFF)

BitmapCanvas.draw = function(canvas, x, y, buf, att, fgColor, bgColor)
	local len = buffer.len(buf)
	local cellHeight = canvas.cellSize * 2
	if (len * 8) % cellHeight ~= 0 then
		error(`Unsupported glyph size: {len}`)
	end

	local pixelWidth = (len * 8) // cellHeight
	local pixelOffsetX = x * canvas.cellSize
	local pixelOffsetY = y * cellHeight
	local canvasWidth = canvas.width * canvas.cellSize

	-- Overdraw check
	-- Only checks if the overdraw happens past the size of the canvas table to prevent out of bounds errors
	if pixelOffsetX + pixelWidth > canvasWidth and pixelOffsetY + cellHeight >= canvas.height * canvas.cellSize then
		-- TODO: Handle overdraw without an error, but also without branching in the loops below
		error(`Overdraw detected, buffer length: {len}, cell size: {canvas.cellSize}, x: {x}, y: {y}`)
	end

	local canvasBuffer = canvas.canvas

	if bgColor then
		for byteIndex = 0, len - 1, 4 do
			local num = buffer.readu32(buf, byteIndex)
			for bitIndex = 0, 31 do
				local bit = bit32.extract(num, 31 - bitIndex, 1)
				local idx = byteIndex * 8 + bitIndex
				local x1 = pixelOffsetX + idx % pixelWidth
				local y1 = pixelOffsetY + (idx // pixelWidth)
				local pos = (x1 + y1 * canvasWidth) * 4

				buffer.writeu32(canvasBuffer, pos, fgColor * bit + bgColor * (1 - bit))
			end
		end
	else
		for byteIndex = 0, len - 1, 4 do
			local num = buffer.readu32(buf, byteIndex)
			for bitIndex = 0, 31 do
				local bit = bit32.extract(num, 31 - bitIndex, 1)
				local idx = byteIndex * 8 + bitIndex
				local x1 = pixelOffsetX + idx % pixelWidth
				local y1 = pixelOffsetY + (idx // pixelWidth)
				local pos = (x1 + y1 * canvasWidth) * 4

				local bg = buffer.readu32(canvasBuffer, pos)

				buffer.writeu32(canvasBuffer, pos, fgColor * bit + bg * (1 - bit))
			end
		end
	end

	if att.underline then
		BitmapCanvas.draw(canvas, x, y, underlineGlyph, {} :: any, fgColor)
	end
end

BitmapCanvas.fillCell = function(canvas, x, y, color)
	local buf = canvas.canvas
	local pixelOffsetX = x * canvas.cellSize
	local pixelOffsetY = y * canvas.cellSize * 2
	local canvasWidth = canvas.width * canvas.cellSize

	for y0 = 0, canvas.cellSize * 2 - 1 do
		for x0 = 0, canvas.cellSize - 1 do
			local x1 = pixelOffsetX + x0
			local y1 = pixelOffsetY + y0
			local i = (x1 + y1 * canvasWidth) * 4
			buffer.writeu32(buf, i, color)
		end
	end
end

BitmapCanvas.fillLines = function(canvas, startY, lineCount, color)
	local buf = canvas.canvas
	local lineLength = canvas.length / canvas.height
	local startOffset = startY * lineLength
	local totalPixelLength = lineLength / 4 * lineCount

	-- Instead of writing to each pixel individually, the code below
	-- copies previously written chunks, so on every iteration it copies
	-- twice as much
	local n = math.log(totalPixelLength, 2)
	buffer.writeu32(buf, startOffset, color)
	local count = 1
	for i = 1, n do
		buffer.copy(buf, startOffset + count * 4, buf, startOffset, count * 4)
		count *= 2
	end

	local remaining = totalPixelLength - count
	if remaining > 0 then
		buffer.copy(buf, startOffset + count * 4, buf, startOffset, remaining * 4)
	end
end

BitmapCanvas.fillScreen = function(canvas, color)
	BitmapCanvas.fillLines(canvas, 0, canvas.height, color)
end

--- Moves the canvas **DOWN**, adding lines on the top
BitmapCanvas.shiftDown = function(canvas, lines, topMargin, bottomMargin, color)
	local availableLines = canvas.height - topMargin - bottomMargin

	if availableLines < 1 then
		warn("Margins exceed canvas height, not shifting")
		return
	end

	local clampedLines = math.clamp(lines, 1, availableLines)
	local lineHeight = canvas.length / canvas.height
	local startOffset = topMargin * lineHeight
	local lineOffset = startOffset + clampedLines * lineHeight
	local endOffset = (canvas.height - bottomMargin) * lineHeight

	local buf = canvas.canvas
	buffer.copy(buf, lineOffset, buf, startOffset, endOffset - lineOffset)

	BitmapCanvas.fillLines(canvas, topMargin, clampedLines, color)
end

--- Moves the canvas **UP**, adding lines on the bottom
BitmapCanvas.shiftUp = function(canvas, lines, topMargin, bottomMargin, color)
	local availableLines = canvas.height - topMargin - bottomMargin

	if availableLines < 1 then
		warn("Margins exceed canvas height, not shifting")
		return
	end

	local clampedLines = math.clamp(lines, 1, availableLines)
	local lineHeight = canvas.length / canvas.height
	local startOffset = topMargin * lineHeight
	local lineOffset = startOffset + clampedLines * lineHeight
	local endOffset = (canvas.height - bottomMargin) * lineHeight

	local buf = canvas.canvas
	buffer.copy(buf, startOffset, buf, lineOffset, endOffset - lineOffset)

	BitmapCanvas.fillLines(canvas, canvas.height - bottomMargin - clampedLines, clampedLines, color)
end

BitmapCanvas.shiftRight = function(canvas, y, startX, endX, emptyCellCount, bgColor)
	local width = endX - startX
	local cellsToCopy = width - emptyCellCount

	if cellsToCopy > 0 then
		local cellHeight = canvas.cellSize * 2
		local lineWidth = canvas.length / canvas.height
		local lineOffset = lineWidth / cellHeight
		local startOffset = lineWidth * y + startX * canvas.cellSize * 4
		local buf = canvas.canvas
		for line = 0, cellHeight do
			buffer.copy(
				buf,
				startOffset + lineOffset * line + emptyCellCount * canvas.cellSize * 4,
				buf,
				startOffset + lineOffset * line,
				cellsToCopy * canvas.cellSize * 4
			)
		end
	end

	for x = 0, emptyCellCount - 1 do
		BitmapCanvas.fillCell(canvas, startX + x, y, bgColor)
	end
end

BitmapCanvas.shiftLeft = function(canvas, y, startX, endX, emptyCellCount, bgColor)
	local width = endX - startX
	local cellsToCopy = width - emptyCellCount

	if cellsToCopy > 0 then
		local cellHeight = canvas.cellSize * 2
		local lineWidth = canvas.length / canvas.height
		local lineOffset = lineWidth / cellHeight
		local startOffset = lineWidth * y + startX * canvas.cellSize * 4
		local buf = canvas.canvas
		for line = 0, cellHeight do
			buffer.copy(
				buf,
				startOffset + lineOffset * line,
				buf,
				startOffset + lineOffset * line + emptyCellCount * canvas.cellSize * 4,
				cellsToCopy * canvas.cellSize * 4
			)
		end
	end

	for x = 0, emptyCellCount - 1 do
		BitmapCanvas.fillCell(canvas, endX - x, y, bgColor)
	end
end

BitmapCanvas.createCanvas = function(w, h, cellSize)
	local cs = cellSize or 8
	local len = w * h * cs * cs * 2 * 4
	local canvas: Types.Canvas<BitmapCanvas> = {
		canvas = buffer.create(len),
		length = len,
		width = w,
		height = h,
		cellSize = cs,
	}

	return canvas
end

return table.freeze(BitmapCanvas)


local Types = require("./types")
local BitmapCanvas = require("./bitmap")
local TextLabelCanvas = require("./textlabel")

export type BitmapCanvasType = "Bitmap"
export type TextLabelCanvasType = "TextLabel"
export type CanvasType = BitmapCanvasType | TextLabelCanvasType
export type RGBA = Types.RGBA
export type Canvas<CanvasType> = Types.Canvas<CanvasType>
export type CanvasModule<CanvasType, CharacterData> = Types.CanvasModule<CanvasType, CharacterData>

local CanvasType = table.freeze({
	Bitmap = "Bitmap" :: BitmapCanvasType,
	TextLabel = "TextLabel" :: TextLabelCanvasType,
})

return table.freeze({
	CanvasType = CanvasType,
	Bitmap = BitmapCanvas,
	TextLabel = TextLabelCanvas,
} :: { CanvasType: typeof(CanvasType), [CanvasType]: CanvasModule<any, any> })


local Types = require("./types")

type TextLabelCanvas = {
	frame: Frame,
	font: {
		Regular: Font,
		Bold: Font,
		Italic: Font,
		BoldItalic: Font,
	},
	lines: { Frame },
	labels: { TextLabel },
}

local TextLabelCanvas = {} :: Types.CanvasModule<TextLabelCanvas, string>

local function abgrToColor3(abgr: number): Color3
	local r = bit32.band(abgr, 0xff)
	local g = bit32.band(bit32.rshift(abgr, 8), 0xff)
	local b = bit32.band(bit32.rshift(abgr, 16), 0xff)

	return Color3.fromRGB(r, g, b)
end

local function copyLabel(targetLabel: TextLabel, sourceLabel: TextLabel): ()
	targetLabel.RichText = sourceLabel.RichText
	targetLabel.Text = sourceLabel.Text
	targetLabel.TextColor3 = sourceLabel.TextColor3
	targetLabel.BackgroundColor3 = sourceLabel.BackgroundColor3
	targetLabel.FontFace = sourceLabel.FontFace
end

local function clearLabel(label: TextLabel, clr: Color3): ()
	label.Text = ""
	label.BackgroundColor3 = clr
end

function TextLabelCanvas.draw(canvas, x, y, char, att, fg, bg)
	local label = canvas.canvas.labels[y * canvas.width + x + 1]
	label.TextColor3 = abgrToColor3(fg)
	if bg then
		label.BackgroundColor3 = abgrToColor3(bg)
	end
	if att.underline then
		label.RichText = true
		label.Text = "<u>" .. char .. "</u>"
	else
		label.RichText = false
		label.Text = char
	end
	local fonts = canvas.canvas.font
	if att.bold and att.italic then
		label.FontFace = fonts.BoldItalic
	elseif att.bold then
		label.FontFace = fonts.Bold
	elseif att.italic then
		label.FontFace = fonts.Italic
	else
		label.FontFace = fonts.Regular
	end
end

function TextLabelCanvas.fillCell(canvas, x, y, bg)
	clearLabel(canvas.canvas.labels[y * canvas.width + x + 1], abgrToColor3(bg))
end

function TextLabelCanvas.fillLines(canvas, startY, count, bg)
	local clr = abgrToColor3(bg)
	local labels = canvas.canvas.labels
	for i = startY * canvas.width + 1, (startY + count) * canvas.width do
		clearLabel(labels[i], clr)
	end
end

function TextLabelCanvas.fillScreen(canvas, bg)
	TextLabelCanvas.fillLines(canvas, 0, canvas.height, bg)
end

function TextLabelCanvas.shiftDown(canvas, lines, topMargin, bottomMargin, bg)
	local availableLines = canvas.height - topMargin - bottomMargin

	if availableLines < 1 then
		warn("Margins exceed canvas height, not shifting")
		return
	end

	local labels = canvas.canvas.labels

	local clampedLines = math.clamp(lines, 1, availableLines)
	local startOffset = canvas.width * topMargin + 1
	local emptyLinesOffset = canvas.width * clampedLines
	local midOffset = startOffset + emptyLinesOffset
	local endOffset = canvas.width * (canvas.height - bottomMargin)

	for i = endOffset, midOffset, -1 do
		copyLabel(labels[i], labels[i - emptyLinesOffset])
	end

	local clr = abgrToColor3(bg)

	for i = startOffset, midOffset - 1 do
		clearLabel(labels[i], clr)
	end
end

function TextLabelCanvas.shiftUp(canvas, lines, topMargin, bottomMargin, bg)
	local availableLines = canvas.height - topMargin - bottomMargin

	if availableLines < 1 then
		warn("Margins exceed canvas height, not shifting")
		return
	end

	local labels = canvas.canvas.labels

	local clampedLines = math.clamp(lines, 1, availableLines)
	local startOffset = canvas.width * topMargin + 1
	local endOffset = canvas.width * (canvas.height - bottomMargin)
	local emptyLinesOffset = canvas.width * clampedLines
	local midOffset = endOffset - emptyLinesOffset + 1

	for i = startOffset, midOffset - 1 do
		copyLabel(labels[i], labels[i + emptyLinesOffset])
	end

	local clr = abgrToColor3(bg)

	for i = midOffset, endOffset do
		clearLabel(labels[i], clr)
	end
end

function TextLabelCanvas.shiftRight(canvas, y, startX, endX, emptyCellCount, bg)
	local clr = abgrToColor3(bg)
	local labels = canvas.canvas.labels

	local lineOffset = canvas.width * y + 1
	local width = endX - startX
	local cellsToCopy = width - emptyCellCount

	for x = 0, cellsToCopy do
		local i = lineOffset + endX - x
		copyLabel(labels[i], labels[i - emptyCellCount])
	end

	for x = 0, emptyCellCount - 1 do
		clearLabel(labels[lineOffset + startX + x], clr)
	end
end

function TextLabelCanvas.shiftLeft(canvas, y, startX, endX, emptyCellCount, bg)
	local clr = abgrToColor3(bg)
	local labels = canvas.canvas.labels

	local lineOffset = canvas.width * y + 1
	local width = endX - startX
	local cellsToCopy = width - emptyCellCount

	for x = 0, cellsToCopy do
		local i = lineOffset + startX + x
		copyLabel(labels[i], labels[i + emptyCellCount])
	end

	for x = 0, emptyCellCount - 1 do
		clearLabel(labels[lineOffset + endX - x], clr)
	end
end

function TextLabelCanvas.createCanvas(w, h, cellSize)
	assert(Instance, "TextLabelCanvas can only be used in Roblox environment!")

	local cs = cellSize or 8
	local len = w * h

	local frame = Instance.new("Frame")
	frame.BackgroundTransparency = 1
	frame.Size = UDim2.fromScale(1, 1)

	local fonts = {
		Regular = Font.new("rbxasset://fonts/families/Inconsolata.json", Enum.FontWeight.Regular, Enum.FontStyle.Normal),
		Bold = Font.new("rbxasset://fonts/families/Inconsolata.json", Enum.FontWeight.Bold, Enum.FontStyle.Normal),
		Italic = Font.new("rbxasset://fonts/families/Inconsolata.json", Enum.FontWeight.Regular, Enum.FontStyle.Italic),
		BoldItalic = Font.new("rbxasset://fonts/families/Inconsolata.json", Enum.FontWeight.Bold, Enum.FontStyle.Italic),
	}

	local lines = table.create(h)
	local labels = table.create(len)
	for line = 1, h do
		local lineFrame = Instance.new("Frame")
		lineFrame.BackgroundTransparency = 1
		lineFrame.Size = UDim2.fromScale(1 / w, 1 / h)
		lineFrame.Position = UDim2.fromScale(0, (line - 1) / h)
		lineFrame.Parent = frame

		lines[line] = lineFrame

		for char = 1, w do
			local label = Instance.new("TextLabel")
			label.BorderSizePixel = 0
			label.BackgroundColor3 = Color3.new(0, 0, 0)
			label.TextColor3 = Color3.new(1, 1, 1)
			label.Size = UDim2.fromScale(1, 1)
			label.Position = UDim2.fromScale(char - 1, 0)
			label.FontFace = fonts.Regular
			label.TextSize = cs * 2
			label.TextScaled = false
			label.TextWrapped = false
			label.TextXAlignment = Enum.TextXAlignment.Center
			label.TextYAlignment = Enum.TextYAlignment.Center
			label.AutoLocalize = false
			label.Parent = lineFrame

			labels[(line - 1) * w + char] = label
		end
	end

	local canvas: Types.Canvas<TextLabelCanvas> = {
		canvas = {
			frame = frame,
			font = fonts,
			lines = lines,
			labels = labels,
		},
		length = len,
		width = w,
		height = h,
		cellSize = cs,
	}

	return canvas
end

return table.freeze(TextLabelCanvas)


export type RGBA = number

export type Canvas<CanvasType> = {
	canvas: CanvasType,
	length: number,
	width: number,
	height: number,
	cellSize: number,
}

export type Attributes = {
	underline: boolean,
	bold: boolean,
	italic: boolean,
}

export type CanvasModule<CanvasType, CharacterData> = {
	draw: (canvas: Canvas<CanvasType>, x: number, y: number, data: CharacterData, att: Attributes, fg: RGBA, bg: RGBA?) -> (),
	fillCell: (canvas: Canvas<CanvasType>, x: number, y: number, bg: RGBA) -> (),
	fillLines: (canvas: Canvas<CanvasType>, y: number, count: number, bg: RGBA) -> (),
	fillScreen: (canvas: Canvas<CanvasType>, bg: RGBA) -> (),
	shiftDown: (canvas: Canvas<CanvasType>, count: number, topMargin: number, bottomMargin: number, bg: RGBA) -> (),
	shiftUp: (canvas: Canvas<CanvasType>, count: number, topMargin: number, bottomMargin: number, bg: RGBA) -> (),
	shiftRight: (canvas: Canvas<CanvasType>, y: number, x1: number, x2: number, count: number, bg: RGBA) -> (),
	shiftLeft: (canvas: Canvas<CanvasType>, y: number, x1: number, x2: number, count: number, bg: RGBA) -> (),
	createCanvas: (w: number, h: number, cellSize: number?) -> Canvas<CanvasType>,
}

return {}


local ParserState = table.freeze({
	GROUND = 0,
	ESCAPE = 1,
	ESCAPE_INTERMEDIATE = 2,
	CSI_ENTRY = 3,
	CSI_PARAM = 4,
	CSI_INTERMEDIATE = 5,
	CSI_IGNORE = 6,
	SOS_PM_APC_STRING = 7,
	OSC_STRING = 8,
	DCS_ENTRY = 9,
	DCS_PARAM = 10,
	DCS_IGNORE = 11,
	DCS_INTERMEDIATE = 12,
	DCS_PASSTHROUGH = 13,
})

local ParserAction = table.freeze({
	IGNORE = 0,
	ERROR = 1,
	PRINT = 2,
	EXECUTE = 3,
	OSC_START = 4,
	OSC_PUT = 5,
	OSC_END = 6,
	CSI_DISPATCH = 7,
	PARAM = 8,
	COLLECT = 9,
	ESC_DISPATCH = 10,
	CLEAR = 11,
	DCS_HOOK = 12,
	DCS_PUT = 13,
	DCS_UNHOOK = 14,
})

local OscState = table.freeze({
	START = 0,
	ID = 1,
	PAYLOAD = 2,
	ABORT = 3,
})

-- C0 control codes
-- See = https://en.wikipedia.org/wiki/C0_and_C1_control_codes
local C0 = table.freeze({
	--  Null (Caret = ^@, C = \0)
	NUL = 0x00,
	--  Start of Heading (Caret = ^A)
	SOH = 0x01,
	--  Start of Text (Caret = ^B)
	STX = 0x02,
	--  End of Text (Caret = ^C)
	ETX = 0x03,
	--  End of Transmission (Caret = ^D)
	EOT = 0x04,
	--  Enquiry (Caret = ^E)
	ENQ = 0x05,
	--  Acknowledge (Caret = ^F)
	ACK = 0x06,
	--  Bell (Caret = ^G, C = \a)
	BEL = 0x07,
	--  Backspace (Caret = ^H, C = \b)
	BS = 0x08,
	--  Character Tabulation, Horizontal Tabulation (Caret = ^I, C = \t)
	HT = 0x09,
	--  Line Feed (Caret = ^J, C = \n)
	LF = 0x0a,
	--  Line Tabulation, Vertical Tabulation (Caret = ^K, C = \v)
	VT = 0x0b,
	--  Form Feed (Caret = ^L, C = \f)
	FF = 0x0c,
	--  Carriage Return (Caret = ^M, C = \r)
	CR = 0x0d,
	--  Shift Out (Caret = ^N)
	SO = 0x0e,
	--  Shift In (Caret = ^O)
	SI = 0x0f,
	--  Data Link Escape (Caret = ^P)
	DLE = 0x10,
	--  Device Control One (XON) (Caret = ^Q)
	DC1 = 0x11,
	--  Device Control Two (Caret = ^R)
	DC2 = 0x12,
	--  Device Control Three (XOFF) (Caret = ^S)
	DC3 = 0x13,
	--  Device Control Four (Caret = ^T)
	DC4 = 0x14,
	--  Negative Acknowledge (Caret = ^U)
	NAK = 0x15,
	--  Synchronous Idle (Caret = ^V)
	SYN = 0x16,
	--  End of Transmission Block (Caret = ^W)
	ETB = 0x17,
	--  Cancel (Caret = ^X)
	CAN = 0x18,
	--  End of Medium (Caret = ^Y)
	EM = 0x19,
	--  Substitute (Caret = ^Z)
	SUB = 0x1a,
	--  Escape (Caret = ^[, C = \e)
	ESC = 0x1b,
	--  File Separator (Caret = ^\)
	FS = 0x1c,
	--  Group Separator (Caret = ^])
	GS = 0x1d,
	--  Record Separator (Caret = ^^)
	RS = 0x1e,
	--  Unit Separator (Caret = ^_)
	US = 0x1f,
	--  Space
	SP = 0x20,
	--  Delete (Caret = ^?)
	DEL = 0x7f,
})

-- C1 control codes
-- See = https://en.wikipedia.org/wiki/C0_and_C1_control_codes
local C1 = table.freeze({
	--  padding character
	PAD = 0x80,
	--  High Octet Preset
	HOP = 0x81,
	--  Break Permitted Here
	BPH = 0x82,
	--  No Break Here
	NBH = 0x83,
	--  Index
	IND = 0x84,
	--  Next Line
	NEL = 0x85,
	--  Start of Selected Area
	SSA = 0x86,
	--  End of Selected Area
	ESA = 0x87,
	--  Horizontal Tabulation Set
	HTS = 0x88,
	--  Horizontal Tabulation With Justification
	HTJ = 0x89,
	--  Vertical Tabulation Set
	VTS = 0x8a,
	--  Partial Line Down
	PLD = 0x8b,
	--  Partial Line Up
	PLU = 0x8c,
	--  Reverse Index
	RI = 0x8d,
	--  Single-Shift 2
	SS2 = 0x8e,
	--  Single-Shift 3
	SS3 = 0x8f,
	--  Device Control String
	DCS = 0x90,
	--  Private Use 1
	PU1 = 0x91,
	--  Private Use 2
	PU2 = 0x92,
	--  Set Transmit State
	STS = 0x93,
	--  Destructive backspace, intended to eliminate ambiguity about meaning of BS.
	CCH = 0x94,
	--  Message Waiting
	MW = 0x95,
	--  Start of Protected Area
	SPA = 0x96,
	--  End of Protected Area
	EPA = 0x97,
	--  Start of String
	SOS = 0x98,
	--  Single Graphic Character Introducer
	SGCI = 0x99,
	--  Single Character Introducer
	SCI = 0x9a,
	--  Control Sequence Introducer
	CSI = 0x9b,
	--  String Terminator
	ST = 0x9c,
	--  Operating System Command
	OSC = 0x9d,
	--  Privacy Message
	PM = 0x9e,
	--  Application Program Command
	APC = 0x9f,
})

local DATA_LIMIT = 1024

local NON_ASCII_PRINTABLE = 0xA0

local DEFAULT_TAB_WIDTH = 8

local TableAccess = table.freeze({
	TRANSITION_ACTION_SHIFT = 4,
	TRANSITION_STATE_MASK = 15,
	INDEX_STATE_SHIFT = 8,
})

-- Since buffers use little endian, the numbers have to be written in ABGR format
local ANSI_COLORS = {
	0xff_00_00_00, -- 0, 0, 0
	0xff_00_00_bb, -- 187, 0, 0
	0xff_00_bb_00, -- 0, 187, 0
	0xff_00_bb_bb, -- 187, 187, 0
	0xff_bb_00_00, -- 0, 0, 187
	0xff_bb_00_bb, -- 187, 0, 187
	0xff_bb_bb_00, -- 0, 187, 187
	0xff_bb_bb_bb, -- 187, 187, 187

	-- Bright colors
	0xff_55_55_55, -- 85, 85, 85
	0xff_55_55_ff, -- 255, 85, 85
	0xff_55_ff_55, -- 85, 255, 85
	0xff_55_ff_ff, -- 255, 255, 85
	0xff_ff_55_55, -- 85, 85, 255
	0xff_ff_55_ff, -- 255, 85, 255
	0xff_ff_ff_55, -- 85, 255, 255
	0xff_ff_ff_ff, -- 255, 255, 255
}

-- generate remaining 240 ANSI colors
do
	local v = { 0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff }
	for i = 0, 215 do
		local r = v[(i // 36) % 6 + 1]
		local g = v[(i // 6) % 6 + 1]
		local b = v[i % 6 + 1]

		ANSI_COLORS[i + 17] = 0xff_00_00_00 + b * 0x10000 + g * 0x100 + r
	end

	for i = 0, 23 do
		local c = 8 + i * 10

		ANSI_COLORS[i + 233] = 0xff_00_00_00 + c * 0x10000 + c * 0x100 + c
	end
end

local Constants = table.freeze({
	ParserState = ParserState,
	ParserAction = ParserAction,
	OscState = OscState,
	NON_ASCII_PRINTABLE = NON_ASCII_PRINTABLE,
	TableAccess = TableAccess,
	C0 = C0,
	C1 = C1,
	DCS_DATA_LIMIT = DATA_LIMIT,
	DEFAULT_TAB_WIDTH = DEFAULT_TAB_WIDTH,
	ANSI_COLORS = ANSI_COLORS,
})

return Constants


local Types = require("./types")
local Terminal = require("./terminal")
local parser = require("./parser")
local Constants = require("./constants")
local Canvas = require("./canvas")
local Util = require("./terminal/util")
local parseUnifont = require("./unifont")

local UNIFONT_ASCII = require("./unifont_ascii")

export type Terminal = Types.Terminal

local function stdout(term: Terminal, buf: buffer, bytesToRead: number?): ()
	local n = bytesToRead or buffer.len(buf)

	for i = 0, n - 1 do
		parser.parse(term, buffer.readu8(buf, i))
	end
end

local function getActiveBuffer(term: Terminal): buffer
	assert(term.canvasType == "Bitmap", `Cannot retrieve active buffer: canvas type is {term.canvasType} (Bitmap expected)`)
	return term.currentScreen.canvas.canvas
end

local function getTextLabelContainers(term: Terminal): (Frame, Frame)
	assert(term.canvasType == "TextLabel", `Cannot retrieve TextLabel containers: canvas type is {term.canvasType} (TextLabel expected)`)
	return term.screens.normal.canvas.canvas.frame, term.screens.alternate.canvas.canvas.frame
end

return {
	stdout = stdout,
	getActiveBuffer = getActiveBuffer,
	getTextLabelContainers = getTextLabelContainers,
	createTerminal = Terminal.createTerminal,
	parseUnifont = parseUnifont,
	Constants = Constants,
	Types = Types,
	Canvas = Canvas,
	Util = Util,
	Parser = parser,
	UNIFONT_ASCII = UNIFONT_ASCII,
}


local Constants = require("../constants")
local Types = require("../types")
local TransitionTable = require("./transition_table")
local Unicode = require("../unicode")

local ParserState = Constants.ParserState
local ParserAction = Constants.ParserAction
local OscState = Constants.OscState
local TableAccess = Constants.TableAccess

-- macro for (a << 8 | b)
local function add(a: number, b: number)
	return bit32.bor(bit32.lshift(a, 8), b)
end

local function resetParams(params: Types.Params)
	table.clear(params.params)
	table.clear(params.subParams)
	params._length = 0
	params._isSubParam = false
end

local function addParam(params: Types.Params)
	local len = params._length + 1
	params._isSubParam = false
	params.params[len] = 0
	params._length = len
end

local function addSubParam(params: Types.Params)
	local len = params._length
	params._isSubParam = true
	local t = params.subParams[len]
	if not t then
		params.subParams[len] = { 0 }
	else
		table.insert(t, 0)
	end
end

local function reset(parser: Types.Parser)
	resetParams(parser.params)
	parser.collect = 0
	parser.stringDataLength = 0
	parser.ident = 0
	parser.joinState = 0
end

local function addDigit(params: Types.Params, digit: number)
	local len = params._length
	if params._isSubParam then
		local t = params.subParams[len]
		if #t == 0 then
			table.insert(t, 0)
		end
		local num = t[#t]
		t[#t] = num * 10 + digit
	else
		if len == 0 then
			len = 1
			params._length = 1
		end
		local num = params.params[len] or 0
		params.params[len] = num * 10 + digit
	end
end

local function parse(term: Types.Terminal, code: number): ()
	local parser = term.parser
	local handlers = term.handlers

	if parser.joinState ~= 0 then
		local isReady, newState = Unicode.process(code, parser.joinState)
		if not isReady then
			parser.joinState = newState or 0
			return
		end
		parser.joinState = 0
		if newState then
			handlers.print(term, newState)
			return
		else
			handlers.print(term, Unicode.INVALID_CODEPOINT)
		end
	end

	local idx =
		bit32.bor(bit32.lshift(parser.currentState, TableAccess.INDEX_STATE_SHIFT), math.min(code, Constants.NON_ASCII_PRINTABLE))
	local transition = buffer.readu8(TransitionTable, idx)
	local a = bit32.rshift(transition, TableAccess.TRANSITION_ACTION_SHIFT)
	if a == ParserAction.PRINT then
		local isReady, newState = Unicode.process(code, 0)
		if not isReady then
			parser.joinState = newState or 0
			return
		end
		handlers.print(term, newState or Unicode.INVALID_CODEPOINT)
	elseif a == ParserAction.EXECUTE then
		local handler = handlers.exec[code]
		if handler then
			handler(term)
		else
			handlers.execFallback(term, code)
		end
		parser.joinState = 0
	elseif a == ParserAction.IGNORE then
		-- ignore
	elseif a == ParserAction.ERROR then
		handlers.error(term, code)
	elseif a == ParserAction.CSI_DISPATCH then
		local ident = add(parser.collect, code)
		local handler = handlers.csi[ident]
		if handler then
			handler(term, parser.params)
		else
			handlers.csiFallback(term, ident, parser.params)
		end
		parser.joinState = 0
	elseif a == ParserAction.PARAM then
		-- digits (0x30 - 0x39) and ; (0x3b) and : (0x3a)
		if code == 0x3b then
			addParam(parser.params)
		elseif code == 0x3a then
			addSubParam(parser.params)
		else
			addDigit(parser.params, code - 0x30)
		end
	elseif a == ParserAction.COLLECT then
		parser.collect = add(parser.collect, code)
	elseif a == ParserAction.ESC_DISPATCH then
		local ident = add(parser.collect, code)
		local handler = handlers.esc[ident]
		if handler then
			handler(term)
		else
			handlers.escFallback(term, ident)
		end
		parser.joinState = 0
	elseif a == ParserAction.CLEAR then
		reset(parser)
	elseif a == ParserAction.DCS_HOOK then
		parser.ident = add(parser.collect, code)
		parser.stringDataLength = 0
	elseif a == ParserAction.DCS_PUT then
		if parser.stringDataLength < Constants.DCS_DATA_LIMIT then
			buffer.writeu8(parser.stringData, parser.stringDataLength, code)
			parser.stringDataLength += 1
		end
	elseif a == ParserAction.DCS_UNHOOK then
		local success = code ~= 0x18 and code ~= 0x1a
		if success then
			local handler = handlers.dcs[parser.ident]
			if handler then
				handler(term, parser.stringData, parser.stringDataLength, parser.params)
			else
				handlers.dcsFallback(term, parser.ident, parser.stringData, parser.stringDataLength, parser.params)
			end
		end
		if code == 0x1b then
			transition = bit32.bor(transition, ParserState.ESCAPE)
		end
		reset(parser)
	elseif a == ParserAction.OSC_START then
		parser.oscState = OscState.ID
	elseif a == ParserAction.OSC_PUT then
		if parser.oscState == OscState.ID then
			if code == 0x3b then
				parser.oscState = OscState.PAYLOAD
			elseif code < 0x30 or code > 0x39 then
				parser.oscState = OscState.ABORT
			else
				parser.ident = parser.ident * 10 + (code - 0x30)
			end
		elseif parser.oscState == OscState.PAYLOAD and parser.stringDataLength < Constants.DCS_DATA_LIMIT then
			buffer.writeu8(parser.stringData, parser.stringDataLength, code)
			parser.stringDataLength += 1
		end
	elseif a == ParserAction.OSC_END then
		local success = code ~= 0x18 and code ~= 0x1a
		if success then
			local handler = handlers.osc[parser.ident]
			if handler then
				handler(term, parser.stringData, parser.stringDataLength)
			else
				handlers.oscFallback(term, parser.ident, parser.stringData, parser.stringDataLength)
			end
		end
		if code == 0x1b then
			transition = bit32.bor(transition, ParserState.ESCAPE)
		end
		reset(parser)
	end
	parser.currentState = bit32.band(transition, TableAccess.TRANSITION_STATE_MASK)
end

local function createParser(): Types.Parser
	local parser: Types.Parser = {
		currentState = 0,
		collect = 0,
		params = {
			_isSubParam = false,
			_length = 0,
			params = {},
			subParams = {},
		},
		stringData = buffer.create(Constants.DCS_DATA_LIMIT),
		stringDataLength = 0,
		oscState = OscState.START,
		ident = 0,
		joinState = 0,
	}

	reset(parser)

	return parser
end

return {
	createParser = createParser,
	parse = parse,
	resetParser = reset,
}


local Constants = require("../constants")

local ParserState = Constants.ParserState
local ParserAction = Constants.ParserAction
local TableAccess = Constants.TableAccess
local NON_ASCII_PRINTABLE = Constants.NON_ASCII_PRINTABLE

local len = 4095
local buf = buffer.create(len)

local function setDefault(action: number, next: number): ()
	local i = bit32.bor(bit32.lshift(action, TableAccess.TRANSITION_ACTION_SHIFT), next)
	buffer.fill(buf, 0, i)
end

local function add(code: number, state: number, action: number, next: number): ()
	local k = bit32.bor(bit32.lshift(state, TableAccess.INDEX_STATE_SHIFT), code)
	local v = bit32.bor(bit32.lshift(action, TableAccess.TRANSITION_ACTION_SHIFT), next)
	buffer.writeu8(buf, k, v)
end

local function addRange(from: number, to: number, state: number, action: number, next: number): ()
	local size = to - from
	local k = bit32.bor(bit32.lshift(state, TableAccess.INDEX_STATE_SHIFT), from)
	local v = bit32.bor(bit32.lshift(action, TableAccess.TRANSITION_ACTION_SHIFT), next)
	buffer.fill(buf, k, v, size)
end

local function addMany(codes: { number }, state: number, action: number, next: number): ()
	local v = bit32.bor(bit32.lshift(action, TableAccess.TRANSITION_ACTION_SHIFT), next)
	for _, code in codes do
		local k = bit32.bor(bit32.lshift(state, TableAccess.INDEX_STATE_SHIFT), code)
		buffer.writeu8(buf, k, v)
	end
end

local EXECUTABLES = {}
for i = 0x00, 0x1F do
	if i ~= 0x18 and i ~= 0x1a and i ~= 0x1b then
		table.insert(EXECUTABLES, i)
	end
end

local PRINTABLES_START = 0x20
local PRINTABLES_END = 0x7f

do -- Setting up the table
	setDefault(ParserAction.ERROR, ParserState.GROUND)

	-- Printables
	addRange(PRINTABLES_START, PRINTABLES_END, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND)

	-- Global anywhere rules
	for state = ParserState.GROUND, ParserState.DCS_PASSTHROUGH, 1 do
		addMany({ 0x18, 0x1a, 0x99, 0x9a }, state, ParserAction.EXECUTE, ParserState.GROUND)
		addRange(0x80, 0x90, state, ParserAction.EXECUTE, ParserState.GROUND)
		addRange(0x90, 0x98, state, ParserAction.EXECUTE, ParserState.GROUND)
		add(0x9c, state, ParserAction.IGNORE, ParserState.GROUND) -- ST as terminator
		add(0x1b, state, ParserAction.CLEAR, ParserState.ESCAPE) -- ESC
		add(0x9d, state, ParserAction.OSC_START, ParserState.OSC_STRING) -- OSC
		addMany({ 0x98, 0x9e, 0x9f }, state, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING)
		add(0x9b, state, ParserAction.CLEAR, ParserState.CSI_ENTRY) -- CSI
		add(0x90, state, ParserAction.CLEAR, ParserState.DCS_ENTRY) -- DCS
	end

	-- Rules for executables and 7f
	addMany(EXECUTABLES, ParserState.GROUND, ParserAction.EXECUTE, ParserState.GROUND)
	addMany(EXECUTABLES, ParserState.ESCAPE, ParserAction.EXECUTE, ParserState.ESCAPE)
	add(0x7f, ParserState.ESCAPE, ParserAction.IGNORE, ParserState.ESCAPE)
	addMany(EXECUTABLES, ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING)
	addMany(EXECUTABLES, ParserState.CSI_ENTRY, ParserAction.EXECUTE, ParserState.CSI_ENTRY)
	add(0x7f, ParserState.CSI_ENTRY, ParserAction.IGNORE, ParserState.CSI_ENTRY)
	addMany(EXECUTABLES, ParserState.CSI_PARAM, ParserAction.EXECUTE, ParserState.CSI_PARAM)
	add(0x7f, ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_PARAM)
	addMany(EXECUTABLES, ParserState.CSI_IGNORE, ParserAction.EXECUTE, ParserState.CSI_IGNORE)
	addMany(EXECUTABLES, ParserState.CSI_INTERMEDIATE, ParserAction.EXECUTE, ParserState.CSI_INTERMEDIATE)
	add(0x7f, ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_INTERMEDIATE)
	addMany(EXECUTABLES, ParserState.ESCAPE_INTERMEDIATE, ParserAction.EXECUTE, ParserState.ESCAPE_INTERMEDIATE)
	add(0x7f, ParserState.ESCAPE_INTERMEDIATE, ParserAction.IGNORE, ParserState.ESCAPE_INTERMEDIATE)

	-- OSC
	add(0x5d, ParserState.ESCAPE, ParserAction.OSC_START, ParserState.OSC_STRING)
	addRange(PRINTABLES_START, PRINTABLES_END, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING)
	add(0x7f, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING)
	addMany({ 0x9c, 0x1b, 0x18, 0x1a, 0x07 }, ParserState.OSC_STRING, ParserAction.OSC_END, ParserState.GROUND)
	addRange(0x1c, 0x20, ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING)

	-- sos/pm/apc does nothing
	addMany({ 0x58, 0x5e, 0x5f }, ParserState.ESCAPE, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING)
	addRange(PRINTABLES_START, PRINTABLES_END, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING)
	addMany(EXECUTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING)
	add(0x9c, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.GROUND)
	add(0x7f, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING)

	-- csi entries
	add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY)
	addRange(0x40, 0x7f, ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND)
	addRange(0x30, 0x3c, ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM)
	addMany({ 0x3c, 0x3d, 0x3e, 0x3f }, ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_PARAM)
	addRange(0x30, 0x3c, ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM)
	addRange(0x40, 0x7f, ParserState.CSI_PARAM, ParserAction.CSI_DISPATCH, ParserState.GROUND)
	addMany({ 0x3c, 0x3d, 0x3e, 0x3f }, ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_IGNORE)
	addRange(0x20, 0x40, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE)
	add(0x7f, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE)
	addRange(0x40, 0x7f, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.GROUND)
	addRange(0x20, 0x30, ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE)
	addRange(0x20, 0x30, ParserState.CSI_INTERMEDIATE, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE)
	addRange(0x30, 0x40, ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_IGNORE)
	addRange(0x40, 0x7f, ParserState.CSI_INTERMEDIATE, ParserAction.CSI_DISPATCH, ParserState.GROUND)
	addRange(0x20, 0x30, ParserState.CSI_PARAM, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE)

	-- esc_intermediate
	addRange(0x20, 0x30, ParserState.ESCAPE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE)
	addRange(0x20, 0x30, ParserState.ESCAPE_INTERMEDIATE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE)
	addRange(0x30, 0x7f, ParserState.ESCAPE_INTERMEDIATE, ParserAction.ESC_DISPATCH, ParserState.GROUND)
	addRange(0x30, 0x50, ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND)
	addRange(0x51, 0x58, ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND)
	addMany({ 0x59, 0x5a, 0x5c }, ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND)
	addRange(0x60, 0x7f, ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND)

	-- dcs entry
	add(0x50, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.DCS_ENTRY)
	addMany(EXECUTABLES, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY)
	add(0x7f, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY)
	addRange(0x1c, 0x20, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY)
	addRange(0x20, 0x30, ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE)
	addRange(0x30, 0x3c, ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM)
	addMany({ 0x3c, 0x3d, 0x3e, 0x3f }, ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_PARAM)
	addMany(EXECUTABLES, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE)
	addRange(0x20, 0x80, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE)
	addRange(0x1c, 0x20, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE)
	addMany(EXECUTABLES, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM)
	add(0x7f, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM)
	addRange(0x1c, 0x20, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM)
	addRange(0x30, 0x3c, ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM)
	addMany({ 0x3c, 0x3d, 0x3e, 0x3f }, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_IGNORE)
	addRange(0x20, 0x30, ParserState.DCS_PARAM, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE)
	addMany(EXECUTABLES, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE)
	add(0x7f, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE)
	addRange(0x1c, 0x20, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE)
	addRange(0x20, 0x30, ParserState.DCS_INTERMEDIATE, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE)
	addRange(0x30, 0x40, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_IGNORE)
	addRange(0x40, 0x7f, ParserState.DCS_INTERMEDIATE, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH)
	addRange(0x40, 0x7f, ParserState.DCS_PARAM, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH)
	addRange(0x40, 0x7f, ParserState.DCS_ENTRY, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH)
	addMany(EXECUTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH)
	addRange(PRINTABLES_START, PRINTABLES_END, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH)
	add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH)
	addMany({ 0x1b, 0x9c, 0x18, 0x1a }, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND)

	-- special handling of unicode chars
	add(NON_ASCII_PRINTABLE, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND)
	add(NON_ASCII_PRINTABLE, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING)
	add(NON_ASCII_PRINTABLE, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE)
	add(NON_ASCII_PRINTABLE, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE)
	add(NON_ASCII_PRINTABLE, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH)
end

local VT500_TRANSITION_TABLE = buf

return VT500_TRANSITION_TABLE


-- macro for (a << 8 | b)
local function add(a: number, b: number)
	return bit32.bor(bit32.lshift(a, 8), b)
end

local function b(char: string)
	return string.byte(char, 1, 1)
end

export type FunctionIdentifier = {
	prefix: string?,
	intermediates: { string }?,
	final: string,
}

local function identifier(id: FunctionIdentifier, finalRange: { number }?): number
	local res = 0

	if id.prefix then
		res = b(id.prefix)
		if res ~= 0 and (res < 0x3c or res > 0x3f) then
			error("prefix must be in range 0x3c .. 0x3f")
		end
	end

	if id.intermediates then
		for i = 1, #id.intermediates do
			local intermediate = b(id.intermediates[i])
			if intermediate < 0x20 or intermediate > 0x2f then
				error("intermediate must be in range 0x20 .. 0x2f")
			end
			res = add(res, intermediate)
		end
	end

	local final = b(id.final)
	local final1 = (finalRange and finalRange[1] or 0x40)
	local final2 = (finalRange and finalRange[2] or 0x7e)
	if final < final1 or final > final2 then
		error(`final must be in range {final1} .. {final2}`)
	end
	res = add(res, final)

	return res
end

local function identifierToString(ident: number): string
	local str = ""
	while ident > 0 do
		str = string.char(bit32.band(ident, 0xff)) .. str
		ident = bit32.rshift(ident, 8)
	end

	return str
end

return table.freeze({
	identifier = identifier,
	identifierToString = identifierToString,
})


local Canvas = require("../canvas")
local Constants = require("../constants")
local Types = require("../types")
local Util = require("./util")
local Parser = require("../parser")
local Charsets = require("../charsets")

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

local FunctionIdentifier = require("./function_identifier")
local fi = FunctionIdentifier.identifier
local identifierToString = FunctionIdentifier.identifierToString
local C0, C1 = Constants.C0, Constants.C1

local Handlers = {
	csi = {},
	dcs = {},
	osc = {},
	esc = {},
	exec = {},
	print = function() end,
	error = function() end,
} :: Types.Handlers

local function fiEsc(params)
	return fi(params, { 0x30, 0x7e })
end

Handlers.print = function(term, codepoint)
	local fgColor = term.fgColor
	local fgIndex = term.fgIndex
	local bgColor = term.bgColor
	local bgIndex = term.bgIndex
	if term.attributes.inverted then
		fgColor, bgColor = bgColor, fgColor
		fgIndex, bgIndex = bgIndex, fgIndex
	end
	if term.attributes.bold and fgIndex ~= 0 then
		fgColor = term.theme[(fgIndex % 8) + 8]
	end
	local charset = term.charset
	if charset and charset[codepoint] then
		codepoint = charset[codepoint]
	end
	local screen = term.currentScreen
	local bitmapGlyph, charWidth
	if term.canvasType == Canvas.CanvasType.TextLabel then
		bitmapGlyph = utf8.char(codepoint)
		charWidth = 1
	else
		local glyph = term.font[codepoint] or term.font[0xFFFD] or error(`Unknown codepoint: {codepoint}`)
		charWidth = buffer.len(glyph) / (screen.canvas.cellSize * 2)
		bitmapGlyph = glyph
	end
	if screen.cursor.x + charWidth > screen.canvas.width then
		if term.decPrivateModes.wraparound then
			local nextY = screen.cursor.y + 1
			if nextY == (screen.canvas.height - term.bottomMargin) then
				nextY -= 1
				Util.scrollUp(term, 1)
			elseif nextY >= screen.canvas.height then
				nextY = screen.canvas.height - 1
			end
			Util.setCursorPosition(term, 0, nextY)
		else
			Util.moveCursor(term, -1, 0)
		end
	end
	local cursor = screen.cursor
	if term.modes.insert then
		Util.shiftRight(term, charWidth)
	end
	Canvas[term.canvasType].draw(screen.canvas, cursor.x, cursor.y, bitmapGlyph, term.attributes, fgColor, bgColor)
	screen.cursor += vector(charWidth, 0, 0)
	term.lastPrintedCodepoint = codepoint
end

Handlers.error = function(term) end
Handlers.csiFallback = function(term, ident, params)
	warn(`Unknown CSI code: {identifierToString(ident)}, params: {table.concat(params, ", ")}`)
end
Handlers.escFallback = function(term, code)
	warn(`Unknown ESC code: {identifierToString(code)}`)
end
Handlers.execFallback = function(term, code)
	warn(`Unknown EXECUTE code: {string.format("%02X", code)}`)
end
Handlers.oscFallback = function(term, ident, data, dataLength)
	local b = buffer.create(dataLength)
	buffer.copy(b, 0, data, 0, dataLength)
	warn(`Unknown OSC code: {ident}, data: {buffer.tostring(b)}`)
end
Handlers.dcsFallback = function(term, ident, data, dataLength, params)
	local b = buffer.create(dataLength)
	buffer.copy(b, 0, data, 0, dataLength)
	warn(`Unknown DCS code: {identifierToString(ident)}, data: {buffer.tostring(b)}, params: {table.concat(params, ", ")}`)
end

----------------
-- CSI HANDLERS

-- ESC [ n @ - insert blanks
Handlers.csi[fi({ final = "@" })] = function(term, params)
	Util.shiftRight(term, params.params[1] or 1)
end

-- ESC [ n A - cursor up
Handlers.csi[fi({ final = "A" })] = function(term, params)
	local amount = params.params[1] or 1
	Util.cursorUp(term, amount)
end

-- ESC [ n B - cursor down
Handlers.csi[fi({ final = "B" })] = function(term, params)
	local amount = params.params[1] or 1
	Util.cursorDown(term, amount)
end

-- ESC [ n C - cursor right
Handlers.csi[fi({ final = "C" })] = function(term, params)
	local amount = params.params[1] or 1
	Util.moveCursor(term, amount, 0)
end

-- ESC [ n D - cursor left
Handlers.csi[fi({ final = "D" })] = function(term, params)
	local amount = params.params[1] or 1
	if amount == 0 then
		amount = 1
	end
	Util.cursorLeft(term, amount)
end

-- ESC [ n E - cursor down and to the beginning of the line
Handlers.csi[fi({ final = "E" })] = function(term, params)
	local amount = params.params[1] or 1
	Util.cursorDown(term, amount)
	Util.setCursorPosition(term, 0, nil)
end

-- ESC [ n F - cursor up and to the beginning of the line
Handlers.csi[fi({ final = "F" })] = function(term, params)
	local amount = params.params[1] or 1
	Util.cursorUp(term, amount)
	Util.setCursorPosition(term, 0, nil)
end

-- ESC [ n G - absolute horizontal position
Handlers.csi[fi({ final = "G" })] = function(term, params)
	local x = params.params[1] or 1
	Util.setCursorPosition(term, x - 1, nil)
end

-- ESC [ n H - cursor position
Handlers.csi[fi({ final = "H" })] = function(term, params)
	local column = params.params[1] or 1
	local row = params.params[2] or 1
	Util.setCursorPosition(term, row - 1, column - 1)
end

-- ESC [ n I - forward tab
Handlers.csi[fi({ final = "I" })] = function(term, params)
	local screen = term.currentScreen

	if screen.cursor.x >= screen.canvas.width then
		return
	end

	local amount = params.params[1] or 1
	for i = 1, amount do
		Util.setCursorPosition(term, Util.getNextTab(screen), nil)
	end
end

-- ESC [ n J - erase in display
Handlers.csi[fi({ final = "J" })] = function(term, params)
	local screen = term.currentScreen
	local mode = params.params[1] or 0

	Util.unsetWrapState(term)

	if mode == 0 then
		for x = screen.cursor.x, screen.canvas.width - 1 do
			Canvas[term.canvasType].fillCell(screen.canvas, x, screen.cursor.y, Util.bgColor(term))
		end
		local lines = screen.canvas.height - screen.cursor.y - 1
		if lines > 0 then
			Canvas[term.canvasType].fillLines(screen.canvas, screen.cursor.y + 1, lines, Util.bgColor(term))
		end
	elseif mode == 1 then
		local lines = screen.cursor.y - 1
		if lines > 0 then
			Canvas[term.canvasType].fillLines(screen.canvas, 0, lines, Util.bgColor(term))
		end
		for x = 0, screen.cursor.x do
			Canvas[term.canvasType].fillCell(screen.canvas, x, screen.cursor.y, Util.bgColor(term))
		end
	elseif mode == 2 or mode == 3 then
		Canvas[term.canvasType].fillScreen(screen.canvas, Util.bgColor(term))
	end
end

-- ESC [ ? n J - erase in display (protected mode, not implemented here)
Handlers.csi[fi({ prefix = "?", final = "J" })] = Handlers.csi[fi({ final = "J" })]

-- ESC [ n K - erase in line
Handlers.csi[fi({ final = "K" })] = function(term, params)
	local screen = term.currentScreen
	local mode = params.params[1] or 0

	Util.unsetWrapState(term)

	if mode == 0 then
		for x = screen.cursor.x, screen.canvas.width - 1 do
			Canvas[term.canvasType].fillCell(screen.canvas, x, screen.cursor.y, Util.bgColor(term))
		end
	elseif mode == 1 then
		for x = 0, screen.cursor.x do
			Canvas[term.canvasType].fillCell(screen.canvas, x, screen.cursor.y, Util.bgColor(term))
		end
	elseif mode == 2 then
		Canvas[term.canvasType].fillLines(screen.canvas, screen.cursor.y, 1, Util.bgColor(term))
	end
end

-- ESC [ ? n K - erase in line (protected mode, not implemented here)
Handlers.csi[fi({ prefix = "?", final = "K" })] = Handlers.csi[fi({ final = "K" })]

-- ESC [ n L - insert lines
Handlers.csi[fi({ final = "L" })] = function(term, params)
	local screen = term.currentScreen
	local amount = params.params[1] or 1
	Util.unsetWrapState(term)

	if screen.cursor.y < term.topMargin or screen.cursor.y > term.bottomMargin then
		return
	end

	Canvas[term.canvasType].shiftDown(screen.canvas, amount, screen.cursor.y, term.bottomMargin, Util.bgColor(term))
	Util.setCursorPosition(term, 0, nil)
end

-- ESC [ n M - delete lines
Handlers.csi[fi({ final = "M" })] = function(term, params)
	local screen = term.currentScreen
	local amount = params.params[1] or 1
	Util.unsetWrapState(term)

	if screen.cursor.y < term.topMargin or screen.cursor.y > term.bottomMargin then
		return
	end

	Canvas[term.canvasType].shiftUp(screen.canvas, amount, screen.cursor.y, term.bottomMargin, Util.bgColor(term))
	Util.setCursorPosition(term, 0, nil)
end

-- ESC [ n P - delete chars
Handlers.csi[fi({ final = "P" })] = function(term, params)
	local screen = term.currentScreen
	local amount = params.params[1] or 1
	Util.unsetWrapState(term)
	Canvas[term.canvasType].shiftLeft(
		screen.canvas,
		screen.cursor.y,
		screen.cursor.x,
		screen.canvas.width - 1,
		amount,
		Util.bgColor(term)
	)
end

-- ESC [ n S - scroll up
Handlers.csi[fi({ final = "S" })] = function(term, params)
	local amount = params.params[1] or 1
	Util.scrollUp(term, amount)
end

-- ESC [ n T - scroll down
Handlers.csi[fi({ final = "T" })] = function(term, params)
	local amount = params.params[1] or 1
	Util.scrollDown(term, amount)
end

-- ESC [ n X - erase chars
Handlers.csi[fi({ final = "X" })] = function(term, params)
	local screen = term.currentScreen
	local amount = params.params[1] or 1
	local endX = math.min(screen.cursor.x + amount - 1, screen.canvas.width - 1)
	Util.unsetWrapState(term)
	for x = screen.cursor.x, endX do
		Canvas[term.canvasType].fillCell(screen.canvas, x, screen.cursor.y, Util.bgColor(term))
	end
end

-- ESC [ n Z - cursor backward tab
Handlers.csi[fi({ final = "Z" })] = function(term, params)
	local screen = term.currentScreen

	if screen.cursor.x <= 0 then
		return
	end

	local amount = params.params[1] or 1
	for i = 1, amount do
		Util.setCursorPosition(term, Util.getPreviousTab(screen), nil)
	end
end

-- ESC [ n ` - cursor horizontal pos
Handlers.csi[fi({ final = "`" })] = function(term, params)
	local x = params.params[1] or 1
	Util.setCursorPosition(term, x - 1, nil)
end

-- ESC [ n a - move cursor horizontally
Handlers.csi[fi({ final = "a" })] = function(term, params)
	local amount = params.params[1] or 1
	Util.moveCursor(term, amount, 0)
end

-- ESC [ n b - repeat previously printed character
Handlers.csi[fi({ final = "b" })] = function(term, params)
	local amount = params.params[1] or 1
	local code = term.lastPrintedCodepoint
	for i = 1, amount do
		Handlers.print(term, code)
	end
end

-- ESC [ n c - primary device attributes
Handlers.csi[fi({ final = "c" })] = function(term, params)
	warn("FIXME: Program requested Primary Device Attributes (DA1)")
end

-- ESC [ > n c - secondary device attributes
Handlers.csi[fi({ prefix = ">", final = "c" })] = function(term, params)
	warn("FIXME: Program requested Secondary Device Attributes (DA2)")
end

-- ESC [ = n c - tertiary device attributes
Handlers.csi[fi({ prefix = "=", final = "c" })] = function(term, params)
	warn("FIXME: Program requested Tertiary Device Attributes (DA3)")
end

-- ESC [ n d - absolute vertical pos
Handlers.csi[fi({ final = "d" })] = function(term, params)
	local y = math.max(params.params[1] or 1, 1)
	Util.unsetWrapState(term)
	Util.setCursorPosition(term, nil, y - 1)
end

-- ESC [ n e - move cursor vertically
Handlers.csi[fi({ final = "e" })] = function(term, params)
	local amount = params.params[1] or 1
	Util.unsetWrapState(term)
	Util.moveCursor(term, 0, amount)
end

-- ESC [ n f - cursor position, alias for ESC [ n H
Handlers.csi[fi({ final = "f" })] = Handlers.csi[fi({ final = "H" })]

-- ESC [ n g - tab clear
Handlers.csi[fi({ final = "g" })] = function(term, params)
	local screen = term.currentScreen
	local mode = params.params[1] or 0
	if mode == 0 then
		local x = screen.cursor.x
		for i, v in term.currentScreen.tabs do
			if v == x then
				table.remove(term.currentScreen.tabs, i)
			elseif v > x then
				break
			end
		end
	elseif mode == 3 then
		table.clear(term.currentScreen.tabs)
	end
end

-- ESC [ [n] h - set mode
Handlers.csi[fi({ final = "h" })] = function(term, params)
	for i, v in params.params do
		if v == 4 then
			term.modes.insert = true
		elseif v == 20 then
			term.options.convertEol = true
		end
	end
end

-- ESC[ ? [n] h - set private mode
Handlers.csi[fi({ prefix = "?", final = "h" })] = function(term, params)
	for i, v in params.params do
		if v == 1 then
			term.decPrivateModes.applicationCursorKeys = true
			term:changeCursorKeyFormat(true)
		elseif v == 6 then
			term.decPrivateModes.origin = true
			Util.setCursorPosition(term, 0, 0)
		elseif v == 7 then
			term.decPrivateModes.wraparound = true
		elseif v == 12 then
			term.options.cursorBlink = true
		elseif v == 45 then
			term.decPrivateModes.reverseWraparound = true
		elseif v == 25 then
			term.cursorHidden = true
			term:changeCursorVisibility(true)
		elseif v == 2004 then
			term.decPrivateModes.bracketedPasteMode = true
			term:changeBracketedPasteMode(true)
		elseif v == 1048 or v == 1049 then
			Util.saveCursor(term.currentScreen)
		end

		-- Separate if check so that 1049 would additionally call `saveCursor` above
		if v == 47 or v == 1047 or v == 1049 then
			Util.switchToAltScreen(term)
		end
	end
end

-- ESC [ [n] l - reset mode
Handlers.csi[fi({ final = "l" })] = function(term, params)
	for i, v in params.params do
		if v == 4 then
			term.modes.insert = false
		elseif v == 20 then
			term.options.convertEol = false
		end
	end
end

-- ESC [ ? [n] l - reset private mode
Handlers.csi[fi({ prefix = "?", final = "l" })] = function(term, params)
	for i, v in params.params do
		if v == 1 then
			term.decPrivateModes.applicationCursorKeys = false
			term:changeCursorKeyFormat(false)
		elseif v == 6 then
			term.decPrivateModes.origin = false
			Util.setCursorPosition(term, 0, 0)
		elseif v == 7 then
			term.decPrivateModes.wraparound = false
		elseif v == 12 then
			term.options.cursorBlink = false
		elseif v == 45 then
			term.decPrivateModes.reverseWraparound = false
		elseif v == 25 then
			term.cursorHidden = false
			term:changeCursorVisibility(false)
		elseif v == 2004 then
			term.decPrivateModes.bracketedPasteMode = false
			term:changeBracketedPasteMode(false)
		elseif v == 1048 or v == 1049 then
			Util.restoreCursor(term.currentScreen)
		end

		if v == 47 or v == 1047 or v == 1049 then
			Util.switchToNormalScreen(term)
		end
	end
end

-- ESC [ [n] m - set graphics rendition (SGR)
local function sgr0(term)
	term.attributes.bold = false
	term.attributes.underline = false
	term.attributes.inverted = false
	term.fgIndex = 8
	term.bgIndex = 1
	term.fgColor = term.theme[term.fgIndex]
	term.bgColor = term.theme[term.bgIndex]
end
Handlers.csi[fi({ final = "m" })] = function(term, params)
	if params._length == 0 or (params._length == 1 and params.params[1] == 0) then
		sgr0(term)
	end
	local i = 1
	while i <= params._length do
		local param = params.params[i]
		if param >= 30 and param <= 37 then
			term.fgIndex = (param % 10) + 1
			term.fgColor = term.theme[term.fgIndex]
		elseif param >= 40 and param <= 47 then
			term.bgIndex = (param % 10) + 1
			term.bgColor = term.theme[term.bgIndex]
		elseif param >= 90 and param <= 97 then
			term.fgIndex = (param % 10) + 9
			term.fgColor = term.theme[term.fgIndex]
		elseif param >= 100 and param <= 107 then
			term.bgIndex = (param % 10) + 9
			term.bgColor = term.theme[term.bgIndex]
		elseif param == 0 then
			sgr0(term)
		elseif param == 1 then
			term.attributes.bold = true
		elseif param == 3 then
			term.attributes.italic = true
		elseif param == 4 then
			term.attributes.underline = true
		elseif param == 7 then
			term.attributes.inverted = true
		elseif param == 22 then
			term.attributes.bold = false
		elseif param == 23 then
			term.attributes.italic = false
		elseif param == 24 then
			term.attributes.underline = false
		elseif param == 27 then
			term.attributes.inverted = false
		elseif param == 38 or param == 48 then
			local clr = 0xff000000
			local t = params.params
			local offset = i
			local mode = 0
			if params.subParams[i] then
				t = params.subParams[i]
				mode = t[1] or 0
				offset = 2
			elseif params.subParams[i + 1] then
				t = params.subParams[i + 1]
				mode = params.params[i + 1]
				offset = 1
				i += 1
			else
				t = params.params
				offset = i + 2
				mode = params.params[i + 1]
				if mode == 2 then
					i += 3
				elseif mode == 5 then
					i += 1
				end
				i += 1
			end
			if mode == 2 then
				local r = t[offset + 0] or 0
				local g = t[offset + 1] or 0
				local b = t[offset + 2] or 0
				clr = r * 0x1 + g * 0x100 + b * 0x10000 + 0xff000000
			elseif mode == 5 then
				clr = term.theme[(t[offset + 0] or 0) + 1] or clr
			end
			if param == 38 then
				term.fgColor = clr
				term.fgIndex = 0
			else
				term.bgColor = clr
				term.bgIndex = 0
			end
		elseif param == 39 then
			term.fgIndex = 8
			term.fgColor = term.theme[8]
		elseif param == 49 then
			term.bgIndex = 1
			term.bgColor = term.theme[1]
		else
			warn(`Unimplemented SGR param: {param}`)
		end
		i += 1
	end
end

-- ESC [ ! p - soft reset
Handlers.csi[fi({ intermediates = { "!" }, final = "n" })] = function(term)
	term.changeCursorVisibility(term, true)
	term.topMargin = 0
	term.bottomMargin = 0

	-- Call SGR 0
	sgr0(term)

	term.charset = nil
	term.charsetGLevel = 0
	table.clear(term.charsets)

	term.decPrivateModes.origin = false
end

-- ESC [ n r - set top and bottom margins
Handlers.csi[fi({ final = "r" })] = function(term, params)
	local height = term.currentScreen.canvas.height
	local mTop = params.params[1] or 1
	local mBottom = params.params[2] or height
	if mBottom == 0 or mBottom > height then
		mBottom = height
	end
	if mTop < mBottom then
		term.topMargin = mTop - 1
		term.bottomMargin = height - mBottom
		Util.setCursorPosition(term, 0, 0)
	else
		term.topMargin = 0
		term.bottomMargin = height
	end
end

-- ESC [ n s - save cursor
Handlers.csi[fi({ final = "s" })] = function(term, params)
	Util.saveCursor(term.currentScreen)
end

----------------
-- EXEC HANDLERS

Handlers.exec[C0.BEL] = function(term)
	local bell = term.bell
	if bell then
		bell(term)
	end
end

Handlers.exec[C0.LF] = function(term)
	if term.options.convertEol then
		term.currentScreen.cursor = vector(0, term.currentScreen.cursor.y, 0)
	end
	Util.cursorDownWithScroll(term)
end
Handlers.exec[C0.VT] = Handlers.exec[C0.LF]
Handlers.exec[C0.FF] = Handlers.exec[C0.LF]

Handlers.exec[C0.CR] = function(term)
	term.currentScreen.cursor = vector(0, term.currentScreen.cursor.y, 0)
end

Handlers.exec[C0.BS] = function(term)
	Util.cursorLeft(term, 1)
end

Handlers.exec[C0.HT] = function(term)
	Util.setCursorPosition(term, Util.getNextTab(term.currentScreen), nil)
end

Handlers.exec[C1.IND] = function(term)
	Util.cursorDownWithScroll(term)
end

Handlers.exec[C1.RI] = function(term)
	Util.cursorUpWithScroll(term)
end

Handlers.exec[C1.NEL] = function(term)
	Util.setCursorPosition(term, 0, nil)
	Util.cursorDownWithScroll(term)
end

Handlers.exec[C1.HTS] = function(term)
	Util.insertTab(term.currentScreen)
end

----------------
-- OSC HANDLERS

-- icon name and title
Handlers.osc[0] = function(term, buf, len)
	local str = buffer.readstring(buf, 0, len)
	warn("FIXME: Program set icon and title: " .. str)
end

-- icon
Handlers.osc[1] = function(term, buf, len)
	local str = buffer.readstring(buf, 0, len)
	warn("FIXME: Program set icon: " .. str)
end

-- title
Handlers.osc[2] = function(term, buf, len)
	local str = buffer.readstring(buf, 0, len)
	warn("FIXME: Program set title: " .. str)
end

Handlers.osc[4] = function(term, buf, len)
	warn("FIXME: Program requested indexed color")
end

-- hyperlink
Handlers.osc[8] = function(term, buf, len)
	-- ignore it
end

----------------
-- ESC HANDLERS

-- ESC \ -- ignore
Handlers.esc[fiEsc({ final = "\\" })] = function() end

-- ESC 7 - save cursor
Handlers.esc[fiEsc({ final = "7" })] = function(term)
	Util.saveCursor(term.currentScreen)
end

-- ESC 8 - restore cursor
Handlers.esc[fiEsc({ final = "8" })] = function(term)
	Util.restoreCursor(term.currentScreen)
end

-- ESC D - index
Handlers.esc[fiEsc({ final = "D" })] = Handlers.exec[C1.IND]

-- ESC E - next line
Handlers.esc[fiEsc({ final = "E" })] = Handlers.exec[C1.NEL]

-- ESC H - set tab
Handlers.esc[fiEsc({ final = "H" })] = Handlers.exec[C1.HTS]

-- ESC M - reverse index
Handlers.esc[fiEsc({ final = "M" })] = Handlers.exec[C1.RI]

-- ESC = - set keypad application mode
Handlers.esc[fiEsc({ final = "=" })] = function(term)
	term.decPrivateModes.applicationKeypad = true
end

-- ESC > - reset keypad application mode
Handlers.esc[fiEsc({ final = ">" })] = function(term)
	term.decPrivateModes.applicationKeypad = false
end

-- ESC c - full reset
Handlers.esc[fiEsc({ final = "c" })] = function(term)
	-- call soft reset
	Handlers.csi[fi({ intermediates = { "!" }, final = "n" })](term, nil :: any)

	Util.setCursorPosition(term, 0, 0)
	Canvas[term.canvasType].fillScreen(term.screens.normal.canvas, term.bgColor)
	Canvas[term.canvasType].fillScreen(term.screens.alternate.canvas, term.bgColor)

	Parser.resetParser(term.parser)
end

-- ESC n - set G2 charset
Handlers.esc[fiEsc({ final = "n" })] = function(term)
	local g = 2
	term.charsetGLevel = g
	term.charset = term.charsets[g]
end

-- ESC o - set G3 charset
Handlers.esc[fiEsc({ final = "n" })] = function(term)
	local g = 3
	term.charsetGLevel = g
	term.charset = term.charsets[g]
end

-- ESC | - set G3 charset
Handlers.esc[fiEsc({ final = "|" })] = Handlers.esc[fiEsc({ final = "o" })]

-- ESC } - set G2 charset
Handlers.esc[fiEsc({ final = "}" })] = Handlers.esc[fiEsc({ final = "n" })]

-- ESC ~ - set G1 charset
Handlers.esc[fiEsc({ final = "n" })] = function(term)
	local g = 1
	term.charsetGLevel = g
	term.charset = term.charsets[g]
end

-- ESC ( rest - Setup G0 charset with 94 characters
-- ESC ) rest - Setup G1 charset with 94 characters
-- ESC * rest - Setup G2 charset with 94 characters
-- ESC + rest - Setup G3 charset with 94 characters
for g, char in { [0] = "(", ")", "*", "+" } do
	local startId = bit32.lshift(string.byte(char), 8)

	local function registerCharset(id: number, charset: Charsets.Charset?)
		local idx = startId + id
		Handlers.esc[idx] = function(term)
			term.charsets[g] = charset
			if term.charsetGLevel == g then
				term.charset = charset
			end
		end

		-- ESC - rest96 - Setup G1 charset with 96 characters
		-- ESC . rest96 - Setup G2 charset with 96 characters
		if g == 1 or g == 2 then
			local startId2 = bit32.lshift(string.byte(g == 2 and "-" or "."), 8)
			Handlers.esc[startId2 + id] = Handlers.esc[idx]
		end

		-- ESC / rest96 - Setup G3 charset with 96 characters
		-- Does not seem to be supported by xterm.js either
		if g == 3 then
			local startId2 = bit32.lshift(string.byte("/"), 8)
			Handlers.esc[startId2 + id] = function(term) end
		end
	end

	for id, charset in Charsets do
		registerCharset(id, charset)
	end

	registerCharset(string.byte("B"), nil)
end

table.freeze(Handlers.esc)
table.freeze(Handlers.csi)
table.freeze(Handlers.dcs)
table.freeze(Handlers.osc)
table.freeze(Handlers.exec)
table.freeze(Handlers)

return Handlers


local parser = require("../parser")
local Canvas = require("../canvas")
local Types = require("../types")
local Handlers = require("./handlers")
local Constants = require("../constants")
local parseUnifont = require("../unifont")
local UNIFONT_ASCII = require("../unifont_ascii")

local vector = require('../vector')

local function noop() end

local function createScreen(canvasType: Canvas.CanvasType, width: number, height: number): Types.TerminalScreen
	local screen: Types.TerminalScreen = {
		canvas = Canvas[canvasType].createCanvas(width, height, 8),
		cursor = vector(0, 0, 0),
		savedCursor = vector(0, 0, 0),
		tabs = table.create(width / Constants.DEFAULT_TAB_WIDTH),
	}

	return screen
end

local function createTerminal(canvasType: Canvas.CanvasType, width: number, height: number, bitmapFont: { buffer }?, theme: { Canvas.RGBA }?): Types.Terminal
	local normalScreen = createScreen(canvasType, width, height)
	local alternateScreen = createScreen(canvasType, width, height)

	local colors = theme or Constants.ANSI_COLORS
	local font = bitmapFont or parseUnifont(UNIFONT_ASCII)

	local terminal: Types.Terminal = {
		canvasType = canvasType,
		currentScreen = normalScreen,
		screens = {
			normal = normalScreen,
			alternate = alternateScreen,
		},
		parser = parser.createParser(),
		handlers = Handlers,
		font = font,
		charsets = {},
		charsetGLevel = 0,
		charset = nil,
		attributes = {
			bold = false,
			italic = false,
			underline = false,
			inverted = false,
		},
		topMargin = 0,
		bottomMargin = 0,
		lastPrintedCodepoint = 0,
		cursorHidden = false,
		options = {
			convertEol = false,
			cursorBlink = true,
		},
		modes = {
			insert = false,
		},
		decPrivateModes = {
			applicationCursorKeys = false,
			applicationKeypad = false,
			bracketedPasteMode = false,
			origin = false,
			reverseWraparound = false,
			sendFocus = false,
			wraparound = true,
		},
		theme = colors,
		fgColor = colors[8],
		fgIndex = 8,
		bgColor = colors[1],
		bgIndex = 1,

		stdin = noop,
		bell = noop,
		changeCursorKeyFormat = noop,
		changeCursorVisibility = noop,
		changeBracketedPasteMode = noop,
	}

	Canvas[canvasType].fillScreen(normalScreen.canvas, terminal.bgColor)
	Canvas[canvasType].fillScreen(alternateScreen.canvas, terminal.bgColor)

	return terminal
end

return {
	createTerminal = createTerminal,
}


local Types = require("../types")
local Canvas = require("../canvas")
local Constants = require("../constants")

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

local Util = {}

Util.bgColor = function(term: Types.Terminal)
	return if term.attributes.inverted then term.fgColor else term.bgColor
end

Util.fgColor = function(term: Types.Terminal)
	return if term.attributes.inverted then term.bgColor else term.fgColor
end

Util.scrollUp = function(term: Types.Terminal, amount: number)
	local screen = term.currentScreen
	Canvas[term.canvasType].shiftUp(screen.canvas, amount, term.topMargin, term.bottomMargin, Util.bgColor(term))
end

Util.scrollDown = function(term: Types.Terminal, amount: number)
	local screen = term.currentScreen
	Canvas[term.canvasType].shiftDown(screen.canvas, amount, term.topMargin, term.bottomMargin, Util.bgColor(term))
end

Util.unsetWrapState = function(term: Types.Terminal)
	local maxX = term.currentScreen.canvas.width
	if term.currentScreen.cursor.x >= maxX then
		term.currentScreen.cursor = vector(maxX - 1, term.currentScreen.cursor.y, 0)
	end
end

Util.shiftRight = function(term: Types.Terminal, amount: number)
	local screen = term.currentScreen
	Util.unsetWrapState(term)
	Canvas[term.canvasType].shiftRight(screen.canvas, screen.cursor.y, screen.cursor.x, screen.canvas.width - 1, amount, Util.bgColor(term))
end

Util.setCursorPosition = function(term: Types.Terminal, x: number | nil, y: number | nil)
	local screen = term.currentScreen
	local origin = term.decPrivateModes.origin
	local mTop = origin and term.topMargin or 0
	local mBottom = origin and term.bottomMargin or 0
	screen.cursor = vector(
		x and math.clamp(x, 0, screen.canvas.width - 1) or screen.cursor.x,
		y and math.clamp(y and y + mTop or screen.cursor.y, mTop, screen.canvas.height - mBottom - 1) or screen.cursor.y,
		0
	)
end

Util.moveCursor = function(term: Types.Terminal, x: number, y: number)
	local screen = term.currentScreen
	Util.setCursorPosition(term, x ~= 0 and screen.cursor.x + x or nil, y ~= 0 and screen.cursor.y + y or nil)
end

Util.cursorUp = function(term: Types.Terminal, amount: number)
	local diffToTop = term.currentScreen.cursor.y - term.topMargin
	Util.moveCursor(term, 0, diffToTop >= 0 and -math.min(diffToTop, amount) or -amount)
end

Util.cursorDown = function(term: Types.Terminal, amount: number)
	local diffToBottom = term.currentScreen.canvas.height - term.bottomMargin - 1 - term.currentScreen.cursor.y
	Util.moveCursor(term, 0, diffToBottom >= 0 and -math.min(diffToBottom, amount) or -amount)
end

Util.cursorLeft = function(term: Types.Terminal, amount: number)
	local screen = term.currentScreen
	local cursor = screen.cursor
	if not term.decPrivateModes.reverseWraparound or not term.decPrivateModes.wraparound then
		screen.cursor = vector(math.max(0, cursor.x - amount), cursor.y, 0)
	else
		local scroll = (cursor.x - amount) // screen.canvas.width
		screen.cursor =
			vector(math.max(0, cursor.x - amount) % screen.canvas.width, (cursor.y + scroll) % screen.canvas.height, 0)
	end
	Util.unsetWrapState(term)
end

Util.cursorRight = function(term: Types.Terminal, amount: number)
	local screen = term.currentScreen
	local cursor = screen.cursor
	screen.cursor = vector(math.min(screen.canvas.width - 1, cursor.x + amount), cursor.y, 0)
end

Util.cursorDownWithScroll = function(term: Types.Terminal)
	local screen = term.currentScreen
	local cursor = screen.cursor
	if cursor.y + 1 == screen.canvas.height - term.bottomMargin then
		Util.scrollUp(term, 1)
	elseif cursor.y + 1 < screen.canvas.height then
		cursor += vector(0, 1, 0)
	end
	if cursor.x >= screen.canvas.width then
		cursor += vector(-1, 0, 0)
	end
	screen.cursor = cursor
end

Util.cursorUpWithScroll = function(term: Types.Terminal)
	local screen = term.currentScreen
	local cursor = screen.cursor
	if cursor.y == term.topMargin then
		Util.scrollDown(term, 1)
	elseif cursor.y > 0 then
		cursor += vector(0, -1, 0)
	end
	if cursor.x >= screen.canvas.width then
		cursor += vector(-1, 0, 0)
	end
	screen.cursor = cursor
end

Util.saveCursor = function(screen: Types.TerminalScreen)
	screen.savedCursor = screen.cursor
end

Util.restoreCursor = function(screen: Types.TerminalScreen)
	screen.cursor = screen.savedCursor
end

Util.resetTabs = function(screen: Types.TerminalScreen)
	table.clear(screen.tabs)
	for i = 0, screen.canvas.width - 1, Constants.DEFAULT_TAB_WIDTH do
		table.insert(screen.tabs, i)
	end
end

Util.insertTab = function(screen: Types.TerminalScreen, pos: number?)
	local x = pos or screen.cursor.x
	for i, v in screen.tabs do
		if v > x then
			table.insert(screen.tabs, i, x)
			return
		elseif v == x then
			return
		end
	end
end

Util.getNextTab = function(screen: Types.TerminalScreen, pos: number?): number
	local x = pos or screen.cursor.x
	for i, v in screen.tabs do
		if v > x then
			return v
		end
	end
	return screen.canvas.width - 1
end

Util.getPreviousTab = function(screen: Types.TerminalScreen, pos: number?): number
	local x = pos or screen.cursor.x
	for i, v in screen.tabs do
		if v >= x then
			return screen.tabs[i - 1] or 0
		end
	end
	return 0
end

Util.switchToAltScreen = function(term: Types.Terminal)
	if term.currentScreen == term.screens.alternate then
		return
	end
	Canvas[term.canvasType].fillScreen(term.screens.alternate.canvas, term.bgColor)
	term.screens.alternate.cursor = term.screens.normal.cursor
	term.currentScreen = term.screens.alternate
end

Util.switchToNormalScreen = function(term: Types.Terminal)
	if term.currentScreen == term.screens.normal then
		return
	end
	Canvas[term.canvasType].fillScreen(term.screens.alternate.canvas, term.bgColor)
	term.screens.normal.cursor = term.screens.alternate.cursor
	term.currentScreen = term.screens.normal
end

return table.freeze(Util)


local Canvas = require("./canvas")
local Charsets = require("./charsets")

export type Cursor = vector

export type Params = {
	_length: number,
	_isSubParam: boolean,
	params: { number },
	subParams: { [number]: { number } },
	[number]: never,
}

export type Parser = {
	currentState: number,
	collect: number,
	params: Params,
	stringData: buffer,
	stringDataLength: number,
	oscState: number,
	ident: number,
	joinState: number,
}

export type TerminalScreen = {
	canvas: Canvas.Canvas<any>,
	cursor: Cursor,
	savedCursor: Cursor,
	tabs: { number },
}

export type Terminal = {
	canvasType: Canvas.CanvasType,
	currentScreen: TerminalScreen,
	screens: {
		normal: TerminalScreen,
		alternate: TerminalScreen,
	},
	parser: Parser,
	handlers: Handlers,
	font: { buffer },
	charsetGLevel: number,
	charsets: { [number]: Charsets.Charset? },
	charset: Charsets.Charset?,
	attributes: {
		bold: boolean,
		italic: boolean,
		underline: boolean,
		inverted: boolean,
	},
	topMargin: number,
	bottomMargin: number,
	lastPrintedCodepoint: number,
	cursorHidden: boolean,
	modes: {
		insert: boolean,
	},
	options: {
		convertEol: boolean,
		cursorBlink: boolean,
	},
	decPrivateModes: {
		applicationCursorKeys: boolean,
		applicationKeypad: boolean,
		bracketedPasteMode: boolean,
		origin: boolean,
		reverseWraparound: boolean,
		sendFocus: boolean,
		wraparound: boolean,
	},
	theme: { Canvas.RGBA },
	fgColor: Canvas.RGBA,
	fgIndex: number,
	bgColor: Canvas.RGBA,
	bgIndex: number,

	-- externally supplied functions
	stdin: (self: Terminal, str: string) -> (),
	bell: (self: Terminal) -> (),
	changeCursorKeyFormat: (self: Terminal, mode: boolean) -> (),
	changeCursorVisibility: (self: Terminal, visible: boolean) -> (),
	changeBracketedPasteMode: (self: Terminal, enable: boolean) -> (),
}

export type Handlers = {
	csi: { (term: Terminal, params: Params) -> () },
	csiFallback: (term: Terminal, ident: number, params: Params) -> (),
	dcs: { (term: Terminal, data: buffer, dataLength: number, params: Params) -> () },
	dcsFallback: (term: Terminal, ident: number, data: buffer, dataLength: number, params: Params) -> (),
	osc: { (term: Terminal, data: buffer, dataLength: number) -> () },
	oscFallback: (term: Terminal, ident: number, data: buffer, dataLength: number) -> (),
	esc: { (term: Terminal) -> () },
	escFallback: (term: Terminal, code: number) -> (),
	exec: { (term: Terminal) -> () },
	execFallback: (term: Terminal, code: number) -> (),
	print: (term: Terminal, code: number) -> (),
	error: (term: Terminal, code: number) -> (),
}

return table.freeze({})


local CONTINUATION_START = 0x80
local CONTINUATION_END = 0xBF
local INVALID_CODEPOINT = 0xFFFD

local function process(code: number, joinState: number): (boolean, number?)
	if joinState > 0 then
		if code < CONTINUATION_START or code > CONTINUATION_END then
			return true, nil
		end

		local bytesRemaining = bit32.band(joinState, 0b11)
		local codeBits = bit32.band(code, 0b00111111)

		local newState = bit32.bor(joinState, bit32.lshift(codeBits, bytesRemaining * 6))
		if bytesRemaining ~= 0 then
			return false, newState - 1
		end
		return true, newState
	end

	if code < CONTINUATION_START then
		return true, code
	elseif code < 0xC2 then
		return true, nil
	elseif code < 0xE0 then
		return false, bit32.lshift(bit32.band(code, 0b00011111), 6)
	elseif code < 0xF0 then
		return false, bit32.lshift(bit32.band(code, 0b00001111), 12) + 1
	elseif code < 0xF5 then
		return false, bit32.lshift(bit32.band(code, 0b00000111), 18) + 2
	end

	return true, nil
end

return {
	process = process,
	INVALID_CODEPOINT = INVALID_CODEPOINT,
}


return (typeof(vector) == "table" and vector.create or vector) :: (x: number, y: number, z: number) -> vector


return function(imageData: buffer, width: number, height: number)
	local extraBytes = width % 4
	local rowBytes = 3 * width + extraBytes
	local rgbSize = height * rowBytes
	local bmpBuffer = buffer.create(54 + rgbSize)

	buffer.writeu16(bmpBuffer, 0, 0x4d42)
	buffer.writeu32(bmpBuffer, 2, buffer.len(bmpBuffer))
	buffer.writeu32(bmpBuffer, 10, 54)
	buffer.writeu32(bmpBuffer, 14, 40)
	buffer.writeu32(bmpBuffer, 18, width)
	buffer.writei32(bmpBuffer, 22, -height)
	buffer.writeu16(bmpBuffer, 26, 1)
	buffer.writeu16(bmpBuffer, 28, 24)
	buffer.writeu32(bmpBuffer, 34, rgbSize)

	local i = 0

	for y = 0, height - 1 do
		for x = 0, width - 1 do
			local p = 54 + y * rowBytes + x * 3

			buffer.writeu8(bmpBuffer, p, buffer.readu8(imageData, i + 2))
			buffer.writeu8(bmpBuffer, p + 1, buffer.readu8(imageData, i + 1))
			buffer.writeu8(bmpBuffer, p + 2, buffer.readu8(imageData, i))

			i += 4
		end
	end

	return bmpBuffer
end


local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components

local Fusion = require(Argon.Packages.Fusion)

local Theme = require(App.Theme)

local Corner = require(Components.Corner)

local New = Fusion.New

type Props = {
	Color: Fusion.CanBeState<Color3>?,
	Transparency: Fusion.CanBeState<number>?,
	Thickness: Fusion.CanBeState<number>?,
	CornerRadius: Fusion.CanBeState<UDim>?,
}

return function(props: Props): { Instance }
	return {
		Corner {},
		New "UIStroke" {
			ApplyStrokeMode = Enum.ApplyStrokeMode.Border,
			Color = props.Color or Theme.Colors.Border,
			Thickness = props.Thickness or Theme.BorderThickness,
			Transparency = props.Transparency or 0,
		},
	}
end


local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components
local Util = Components.Util

local Fusion = require(Argon.Packages.Fusion)

local Theme = require(App.Theme)
local stripProps = require(Util.stripProps)

local Border = require(Components.Border)
local Container = require(Components.Container)

local Hydrate = Fusion.Hydrate
local Children = Fusion.Children

local COMPONENT_ONLY_PROPS = {
	"BackgroundColor",
	"BorderColor",
	"BackgroundTransparency",
	"BorderTransparency",
}

type Props = {
	BackgroundColor: Fusion.CanBeState<Color3>?,
	BorderColor: Fusion.CanBeState<Color3>?,
	BackgroundTransparency: Fusion.CanBeState<number>?,
	BorderTransparency: Fusion.CanBeState<number>?,
	[any]: any,
}

return function(props: Props): Frame
	return Hydrate(Container {
		Size = UDim2.fromOffset(120, Theme.CompSizeY.Large),
		BackgroundColor3 = props.BackgroundColor or Theme.Colors.Background,
		BackgroundTransparency = props.BackgroundTransparency or 0,
		[Children] = {
			Border {
				Color = props.BorderColor,
				Transparency = props.BorderTransparency,
			},
		},
	})(stripProps(props, COMPONENT_ONLY_PROPS))
end


local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components
local Util = Components.Util

local Fusion = require(Argon.Packages.Fusion)

local Assets = require(App.Assets)
local Theme = require(App.Theme)
local stripProps = require(Util.stripProps)
local isState = require(Util.isState)

local IconButton = require(Components.IconButton)

local Value = Fusion.Value
local peek = Fusion.peek
local Hydrate = Fusion.Hydrate
local Computed = Fusion.Computed

local COMPONENT_ONLY_PROPS = {
	"Changed",
	"Value",
	"Placeholder",
}

type Props = {
	Changed: ((value: boolean) -> ())?,
	Value: Fusion.CanBeState<boolean>?,
	Placeholder: Fusion.CanBeState<boolean>?,
	[any]: any,
}

return function(props: Props): TextButton
	local value = isState(props.Value) and props.Value or Value(props.Value or false)
	local placeholder = isState(props.Placeholder) and props.Placeholder or Value(props.Placeholder or false)

	return Hydrate(IconButton {
		Size = UDim2.fromOffset(Theme.CompSizeY.Medium, Theme.CompSizeY.Medium),
		Solid = value,
		Blending = false,
		Icon = Computed(function(use)
			return if use(value) and use(placeholder) or use(placeholder) then Assets.Icons.Checkmark else ""
		end),
		Activated = function()
			value:set(not peek(value))

			if props.Changed then
				props.Changed(peek(value))
			end
		end,
	})(stripProps(props, COMPONENT_ONLY_PROPS))
end


local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components
local Util = Components.Util

local Fusion = require(Argon.Packages.Fusion)

local Theme = require(App.Theme)
local Assets = require(App.Assets)
local animate = require(Util.animate)
local getState = require(Util.getState)
local stripProps = require(Util.stripProps)

local Container = require(Components.Container)
local Padding = require(Components.Padding)
local Corner = require(Components.Corner)
local Image = require(Components.Image)
local List = require(Components.List)
local Text = require(Components.Text)
local Box = require(Components.Box)

local New = Fusion.New
local Value = Fusion.Value
local Tween = Fusion.Tween
local Spring = Fusion.Spring
local Hydrate = Fusion.Hydrate
local OnEvent = Fusion.OnEvent
local Computed = Fusion.Computed
local Children = Fusion.Children
local OnChange = Fusion.OnChange
local peek = Fusion.peek

local COMPONENT_ONLY_PROPS = {
	"Title",
	"Color",
	"Expanded",
	"Elements",
}

type Props = {
	Title: string,
	Color: Fusion.CanBeState<Color3>?,
	Expanded: boolean?,
	Elements: { Instance },
}

return function(props: Props): Frame
	local isExpanded = Value(props.Expanded or false)
	local absoluteSize = Value(Vector2.zero)

	local isHovered = Value(false)
	local isPressed = Value(false)

	local state = getState({
		Hovered = isHovered,
		Pressed = isPressed,
	})

	local color = animate(
		Computed(function(use)
			return use(isExpanded) and use(Theme.Colors.ActiveBackground) or use(Theme.Colors.Background)
		end),
		state
	)

	return Hydrate(Box {
		ClipsDescendants = Computed(function(use)
			return not use(isExpanded)
		end),
		AutomaticSize = Enum.AutomaticSize.None,
		Size = Spring(
			Computed(function(use)
				return UDim2.new(1, 0, 0, Theme.CompSizeY.Large + (use(isExpanded) and use(absoluteSize).Y or 0))
			end),
			30
		),
		[Children] = {
			New "TextButton" {
				Text = "",
				AutoButtonColor = false,
				Size = UDim2.new(1, 0, 0, Theme.CompSizeY.Large),
				BackgroundColor3 = color,
				[OnEvent "InputBegan"] = function(input)
					if input.UserInputType == Enum.UserInputType.MouseMovement then
						isHovered:set(true)
					elseif input.UserInputType == Enum.UserInputType.MouseButton1 then
						isPressed:set(true)
					end
				end,
				[OnEvent "InputEnded"] = function(input)
					if input.UserInputType == Enum.UserInputType.MouseMovement then
						isHovered:set(false)
					elseif input.UserInputType == Enum.UserInputType.MouseButton1 then
						isPressed:set(false)
					end
				end,
				[OnEvent "Activated"] = function()
					isExpanded:set(not peek(isExpanded))
				end,
				[Children] = {
					Corner {},
					Container {
						Size = UDim2.fromScale(1, 1),
						[Children] = {
							Padding {},
							Text {
								Text = props.Title,
								FontFace = Theme.Fonts.Bold,
								TextSize = Theme.TextSize.Large,
								TextXAlignment = Enum.TextXAlignment.Left,
								Size = UDim2.fromScale(1, 1),
								Color = props.Color or Theme.Colors.Text,
							},
							Image {
								AnchorPoint = Vector2.new(1, 0.5),
								Position = UDim2.fromScale(1, 0.5),
								Size = UDim2.fromOffset(16, 16),
								Image = Assets.Icons.Expand,
								ImageColor3 = props.Color or Theme.Colors.Text,
								Rotation = Spring(
									Computed(function(use)
										return use(isExpanded) and 180 or 0
									end),
									30
								),
							},
						},
					},
					New "Frame" {
						AnchorPoint = Vector2.new(0, 1),
						Position = UDim2.fromScale(0, 1),
						Size = UDim2.fromScale(1, 0.2),
						BackgroundColor3 = color,
						BorderSizePixel = 0,
						BackgroundTransparency = Tween(
							Computed(function(use)
								return use(isExpanded) and 0 or 1
							end),
							TweenInfo.new(0.15, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut)
						),
					},
				},
			},
			Container {
				Size = UDim2.fromScale(1, 0),
				Position = UDim2.fromOffset(0, Theme.CompSizeY.Large),
				[OnChange "AbsoluteSize"] = function(size)
					absoluteSize:set(size)
				end,
				[Children] = {
					List {
						Spacing = 0,
					},
					props.Elements,
				},
			},
		},
	})(stripProps(props, COMPONENT_ONLY_PROPS))
end


local Argon = script:FindFirstAncestor("Argon")

local Fusion = require(Argon.Packages.Fusion)

local New = Fusion.New
local Hydrate = Fusion.Hydrate

type Props = {
	[any]: any,
}

return function(props: Props): Frame
	return Hydrate(New "Frame" {
		BorderSizePixel = 0,
		BackgroundTransparency = 1,
		AutomaticSize = Enum.AutomaticSize.Y,
	})(props)
end


local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components
local Util = Components.Util

local Fusion = require(Argon.Packages.Fusion)

local Theme = require(App.Theme)
local stripProps = require(Util.stripProps)

local New = Fusion.New
local Hydrate = Fusion.Hydrate

local COMPONENT_ONLY_PROPS = {
	"Radius",
}

type Props = {
	Radius: number?,
	[any]: any,
}

return function(props: Props): UICorner
	return Hydrate(New "UICorner" {
		CornerRadius = UDim.new(0, props.Radius or Theme.CornerRadius),
	})(stripProps(props, COMPONENT_ONLY_PROPS))
end