-- pain2

local scr_x, scr_y = term.getSize()
local mx, my = scr_x/2, scr_y/2		-- midpoint of screen
local keysDown = {}					-- list of all pushed keys
local miceDown = {}					-- list of all clicked mice buttons
local miceQueue = {}				-- queues all mouse events for use with tool engine
local dragPoses = {{{},{}}, {{},{}}, {{},{}}}	-- records initial and current mouse position per button while scrolling

local TICKNO = 0				-- iterates every time main() loops
local flashPaletteOnBar = 0		-- whether or not to flash the dot palette numbers on the bottom bar, 0 is false, greater than 0 is true

-- debug renderer is slower, but the normal one isn't functional yet
local useDebugRenderer = false

local canvas = {
	{{},{},{}}
}

local render
local pain = {
	scrollX = 0,				-- x position of scroll
	scrollY = 0,				-- y position of scroll
	frame = 1,
	dot = 1,
	brushSize = 2,				-- size of brush for tools like brush or line
	barmsg = "Started PAIN.",	-- message shown on the bottom bar for 'barlife' ticks
	barlife = 12,				-- amount of time until barmsg will cease to render
	showBar = true,				-- whether or not to show the bottom bar
	doRender = true,			-- if true, will render and set doRender to false
	isInFocus = true,			-- will not accept any non-mouse input while false
	exportMode = "nft",			-- saving will use this format
	limitOneMouseButton = true,	-- disallows using more than one mouse button at a time
	size = {
		x = 1,
		y = 1,
		width = scr_x,
		height = scr_y
	},
	guideWidth = scr_x,
	guideHeight = scr_y,
	guideLeftAdj = 2,
	dots = {
		[0] = {
			" ",
			" ",
			" "
		},
		[1] = {
			" ",
			"f",
			"0"
		},
		[2] = {
			" ",
			"f",
			"a"
		},
		[3] = {
			" ",
			"f",
			"b"
		},
		[4] = {
			" ",
			"f",
			"c"
		},
		[5] = {
			" ",
			"f",
			"d"
		},
		[6] = {
			" ",
			"f",
			"2"
		},
		[7] = {
			" ",
			"f",
			"3"
		},
		[8] = {
			" ",
			"f",
			"4"
		},
		[9] = {
			" ",
			"f",
			"5"
		},
	},
	tool = "pencil"
}


-- NFTE API START --

local nfte = {}

local tchar = string.char(31)	-- for text colors
local bchar = string.char(30)	-- for background colors
local nchar = string.char(29)	-- for differentiating multiple frames in ANFT

-- every flippable block character that doesn't need a color swap
local xflippable = {
	["\129"] = "\130",
	["\132"] = "\136",
	["\133"] = "\138",
	["\134"] = "\137",
	["\137"] = "\134",
	["\135"] = "\139",
	["\140"] = "\140",
	["\141"] = "\142",
}
-- every flippable block character that needs a color swap
local xinvertable = {
	["\144"] = "\159",
	["\145"] = "\157",
	["\146"] = "\158",
	["\147"] = "\156",
	["\148"] = "\151",
	["\152"] = "\155",
	["\149"] = "\149",
	["\150"] = "\150",
	["\153"] = "\153",
	["\154"] = "\154"
}
for k,v in pairs(xflippable) do
	xflippable[v] = k
end
for k,v in pairs(xinvertable) do
	xinvertable[v] = k
end
local bl = {	-- blit
	[' '] = 0,
	['0'] = 1,
	['1'] = 2,
	['2'] = 4,
	['3'] = 8,
	['4'] = 16,
	['5'] = 32,
	['6'] = 64,
	['7'] = 128,
	['8'] = 256,
	['9'] = 512,
	['a'] = 1024,
	['b'] = 2048,
	['c'] = 4096,
	['d'] = 8192,
	['e'] = 16384,
	['f'] = 32768,
}
local lb = {} 	-- tilb
for k,v in pairs(bl) do
	lb[v] = k
end
local ldchart = {	-- converts colors into a lighter shade
	["0"] = "0",
	["1"] = "4",
	["2"] = "6",
	["3"] = "0",
	["4"] = "0",
	["5"] = "0",
	["6"] = "0",
	["7"] = "8",
	["8"] = "0",
	["9"] = "3",
	["a"] = "2",
	["b"] = "9",
	["c"] = "1",
	["d"] = "5",
	["e"] = "2",
	["f"] = "7"
}

local dlchart = {	-- converts colors into a darker shade
	["0"] = "8",
	["1"] = "c",
	["2"] = "a",
	["3"] = "9",
	["4"] = "1",
	["5"] = "d",
	["6"] = "2",
	["7"] = "f",
	["8"] = "7",
	["9"] = "b",
	["a"] = "7",
	["b"] = "7",
	["c"] = "7",
	["d"] = "7",
	["e"] = "7",
	["f"] = "f"
}
local round = function(num)
	return math.floor(num + 0.5)
end

local deepCopy
deepCopy = function(tbl)
	local output = {}
	for k,v in pairs(tbl) do
		if type(v) == "table" then
			output[k] = deepCopy(v)
		else
			output[k] = v
		end
	end
	return output
end

local function stringWrite(str,pos,ins,exc)
	str, ins = tostring(str), tostring(ins)
	local output, fn1, fn2 = str:sub(1,pos-1)..ins..str:sub(pos+#ins)
	if exc then
		repeat
			fn1, fn2 = str:find(exc,fn2 and fn2+1 or 1)
			if fn1 then
				output = stringWrite(output,fn1,str:sub(fn1,fn2))
			end
		until not fn1
	end
	return output
end

local checkValid = function(image)
	if type(image) == "table" then
		if #image == 3 then
			return (#image[1] == #image[2] and #image[2] == #image[3])
		end
	end
	return false
end

local checkIfANFT = function(image)
	if type(image) == "table" then
		return type(image[1][1]) == "table"
	elseif type(image) == "string" then
		return image:find(nchar) and true or false
	end
end

local getSizeNFP = function(image)
	local xsize = 0
	if type(image) ~= "table" then return 0,0 end
	for y = 1, #image do xsize = math.max(xsize, #image[y]) end
	return xsize, #image
end

nfte.getSize = function(image)
	assert(checkValid(image), "Invalid image.")
	local x, y = 0, #image[1]
	for y = 1, #image[1] do
		x = math.max(x, #image[1][y])
	end
	return x, y
end

nfte.crop = function(image, x1, y1, x2, y2)
	assert(checkValid(image), "Invalid image.")
	local output = {{},{},{}}
	for y = y1, y2 do
		output[1][#output[1]+1] = image[1][y]:sub(x1,x2)
		output[2][#output[2]+1] = image[2][y]:sub(x1,x2)
		output[3][#output[3]+1] = image[3][y]:sub(x1,x2)
	end
	return output
end

local loadImageDataNFT = function(image, background) -- string image
	local output = {{},{},{}} -- char, text, back
	local y = 1
	background = (background or " "):sub(1,1)
	local text, back = " ", background
	local doSkip, c1, c2 = false
	local maxX = 0
	local bx
	for i = 1, #image do
		if doSkip then
			doSkip = false
		else
			output[1][y] = output[1][y] or ""
			output[2][y] = output[2][y] or ""
			output[3][y] = output[3][y] or ""
			c1, c2 = image:sub(i,i), image:sub(i+1,i+1)
			if c1 == tchar then
				text = c2
				doSkip = true
			elseif c1 == bchar then
				back = c2
				doSkip = true
			elseif c1 == "\n" then
				maxX = math.max(maxX, #output[1][y])
				y = y + 1
				text, back = " ", background
			else
				output[1][y] = output[1][y]..c1
				output[2][y] = output[2][y]..text
				output[3][y] = output[3][y]..back
			end
		end
	end
	for y = 1, #output[1] do
		output[1][y] = output[1][y] .. (" "):rep(maxX - #output[1][y])
		output[2][y] = output[2][y] .. (" "):rep(maxX - #output[2][y])
		output[3][y] = output[3][y] .. (background):rep(maxX - #output[3][y])
	end
	return output
end

local loadImageDataNFP = function(image, background)
	local output = {}
	local x, y = 1, 1
	for i = 1, #image do
		output[y] = output[y] or {}
		if bl[image:sub(i,i)] then
			output[y][x] = bl[image:sub(i,i)]
			x = x + 1
		elseif image:sub(i,i) == "\n" then
			x, y = 1, y + 1
		end
	end
	return output
end

nfte.convertFromNFP = function(image, background)
	background = background or " "
	local output = {{},{},{}}
	if type(image) == "string" then
		image = loadImageDataNFP(image)
	end
	local imageX, imageY = getSizeNFP(image)
	local bx
	for y = 1, imageY do
		output[1][y] = ""
		output[2][y] = ""
		output[3][y] = ""
		for x = 1, imageX do
			if image[y][x] then
				bx = (x % #background) + 1
				output[1][y] = output[1][y]..lb[image[y][x] or background:sub(bx,bx)]
				output[2][y] = output[2][y]..lb[image[y][x] or background:sub(bx,bx)]
				output[3][y] = output[3][y]..lb[image[y][x] or background:sub(bx,bx)]
			end
		end
	end
	return output
end

nfte.loadImageData = function(image, background)
	assert(type(image) == "string", "NFT image data must be string.")
	local output = {}
	-- images can be ANFT, which means they have multiple layers
	if checkIfANFT(image) then
		local L, R = 1, 1
		while L do
			R = (image:find(nchar, L + 1) or 0)
			output[#output+1] = loadImageDataNFT(image:sub(L, R - 1), background)
			L = image:find(nchar, R + 1)
			if L then L = L + 2 end
		end
		return output, "anft"
	elseif image:find(tchar) or image:find(bchar) then
		return loadImageDataNFT(image, background), "nft"
	else
		return convertFromNFP(image), "nfp"
	end
end

nfte.loadImage = function(path, background)
	local file = io.open(path, "r")
	if file then
		io.input(file)
		local output, format = loadImageData(io.read("*all"), background)
		io.close()
		return output, format
	else
		error("No such file exists, or is directory.")
	end
end

local unloadImageNFT = function(image)
	assert(checkValid(image), "Invalid image.")
	local output = ""
	local text, back = " ", " "
	local c, t, b
	for y = 1, #image[1] do
		for x = 1, #image[1][y] do
			c, t, b = image[1][y]:sub(x,x), image[2][y]:sub(x,x), image[3][y]:sub(x,x)
			if (t ~= text) or (x == 1) then
				output = output..tchar..t
				text = t
			end
			if (b ~= back) or (x == 1) then
				output = output..bchar..b
				back = b
			end
			output = output..c
		end
		if y ~= #image[1] then
			output = output.."\n"
			text, back = " ", " "
		end
	end
	return output
end

nfte.unloadImage = function(image)
	assert(checkValid(image), "Invalid image.")
	local output = ""
	if checkIfANFT(image) then
		for i = 1, #image do
			output = output .. unloadImageNFT(image[i])
			if i ~= #image then
				output = output .. nchar .. "\n"
			end
		end
	else
		output = unloadImageNFT(image)
	end
	return output
end

nfte.drawImage = function(image, x, y, terminal)
	assert(checkValid(image), "Invalid image.")
	assert(type(x) == "number", "x value must be number, got " .. type(x))
	assert(type(y) == "number", "y value must be number, got " .. type(y))
	terminal = terminal or term.current()
	local cx, cy = terminal.getCursorPos()
	for iy = 1, #image[1] do
		terminal.setCursorPos(x, y + (iy - 1))
		terminal.blit(image[1][iy], image[2][iy], image[3][iy])
	end
	terminal.setCursorPos(cx,cy)
end

nfte.drawImageTransparent = function(image, x, y, terminal)
	assert(checkValid(image), "Invalid image.")
	assert(type(x) == "number", "x value must be number, got " .. type(x))
	assert(type(y) == "number", "y value must be number, got " .. type(y))
	terminal = terminal or term.current()
	local cx, cy = terminal.getCursorPos()
	local c, t, b
	for iy = 1, #image[1] do
		for ix = 1, #image[1][iy] do
			c, t, b = image[1][iy]:sub(ix,ix), image[2][iy]:sub(ix,ix), image[3][iy]:sub(ix,ix)
			if b ~= " " or c ~= " " then
				terminal.setCursorPos(x + (ix - 1), y + (iy - 1))
				terminal.blit(c, t, b)
			end
		end
	end
	terminal.setCursorPos(cx,cy)
end

nfte.drawImageCenter = function(image, x, y, terminal)
	terminal = terminal or term.current()
	local scr_x, scr_y = terminal.getSize()
	local imageX, imageY = getSize(image)
	return drawImage(
		image,
		round(0.5 + (x and x or (scr_x/2)) - imageX/2),
		round(0.5 + (y and y or (scr_y/2)) - imageY/2),
		terminal
	)
end

nfte.drawImageCenterTransparent = function(image, x, y, terminal)
	terminal = terminal or term.current()
	local scr_x, scr_y = terminal.getSize()
	local imageX, imageY = getSize(image)
	return drawImageTransparent(
		image,
		round(0.5 + (x and x or (scr_x/2)) - imageX/2),
		round(0.5 + (y and y or (scr_y/2)) - imageY/2),
		terminal
	)
end

nfte.colorSwap = function(image, text, back)
	assert(checkValid(image), "Invalid image.")
	local output = {{},{},{}}
	for y = 1, #image[1] do
		output[1][y] = image[1][y]
		output[2][y] = image[2][y]:gsub(".", text)
		output[3][y] = image[3][y]:gsub(".", back or text)
	end
	return output
end

nfte.flipX = function(image)
	assert(checkValid(image), "Invalid image.")
	local output = {{},{},{}}
	for y = 1, #image[1] do
		output[1][y] = image[1][y]:gsub(".", xinvertable):gsub(".", xflippable):reverse()
		output[2][y] = ""
		output[3][y] = ""
		for x = 1, #image[1][y] do
			if xinvertable[image[1][y]:sub(x,x)] then
				output[2][y] = image[3][y]:sub(x,x) .. output[2][y]
				output[3][y] = image[2][y]:sub(x,x) .. output[3][y]
			else
				output[2][y] = image[2][y]:sub(x,x) .. output[2][y]
				output[3][y] = image[3][y]:sub(x,x) .. output[3][y]
			end
		end
	end
	return output
end

nfte.flipY = function(image)
	assert(checkValid(image), "Invalid image.")
	local output = {{},{},{}}
	for y = #image[1], 1, -1 do
		output[1][#output[1]+1] = image[1][y]
		output[2][#output[2]+1] = image[2][y]
		output[3][#output[3]+1] = image[3][y]
	end
	return output
end

nfte.makeRectangle = function(width, height, char, text, back)
	assert(type(width) == "number", "width must be number")
	assert(type(height) == "number", "height must be number")
	local output = {{},{},{}}
	for y = 1, height do
		output[1][y] = (char or " "):rep(width)
		output[2][y] = (text or " "):rep(width)
		output[3][y] = (back or " "):rep(width)
	end
	return output
end

nfte.grayOut = function(image)
	assert(checkValid(image), "Invalid image.")
	local output = {{},{},{}}
	local chart = {
		["0"] = "0",
		["1"] = "8",
		["2"] = "8",
		["3"] = "8",
		["4"] = "8",
		["5"] = "8",
		["6"] = "8",
		["7"] = "7",
		["8"] = "8",
		["9"] = "7",
		["a"] = "7",
		["b"] = "7",
		["c"] = "7",
		["d"] = "7",
		["e"] = "7",
		["f"] = "f"
	}
	for y = 1, #image[1] do
		output[1][y] = image[1][y]
		output[2][y] = image[2][y]:gsub(".", chart)
		output[3][y] = image[3][y]:gsub(".", chart)
	end
	return output
end

nfte.lighten = function(image, amount)
	assert(checkValid(image), "Invalid image.")
	if (amount or 1) < 0 then
		return nfte.darken(image, -amount)
	else
		local output = deepCopy(image)
		for i = 1, amount or 1 do
			for y = 1, #output[1] do
				output[1][y] = output[1][y]
				output[2][y] = output[2][y]:gsub(".",ldchart)
				output[3][y] = output[3][y]:gsub(".",ldchart)
			end
		end
		return output
	end
end

nfte.darken = function(image, amount)
	assert(checkValid(image), "Invalid image.")
	if (amount or 1) < 0 then
		return nfte.lighten(image, -amount)
	else
		local output = deepCopy(image)
		for i = 1, amount or 1 do
			for y = 1, #output[1] do
				output[1][y] = output[1][y]
				output[2][y] = output[2][y]:gsub(".",dlchart)
				output[3][y] = output[3][y]:gsub(".",dlchart)
			end
		end
		return output
	end
end

nfte.stretchImage = function(_image, sx, sy, noRepeat)
	assert(checkValid(_image), "Invalid image.")
	local output = {{},{},{}}
	local image = deepCopy(_image)
	if sx < 0 then image = flipX(image) end
	if sy < 0 then image = flipY(image) end
	sx, sy = math.abs(sx), math.abs(sy)
	local imageX, imageY = getSize(image)
	local tx, ty
	if sx == 0 or sy == 0 then
		for y = 1, math.max(sy, 1) do
			output[1][y] = ""
			output[2][y] = ""
			output[3][y] = ""
		end
		return output
	else
		for y = 1, sy do
			for x = 1, sx do
				tx = round((x / sx) * imageX)
				ty = math.ceil((y / sy) * imageY)
				if not noRepeat then
					output[1][y] = (output[1][y] or "")..image[1][ty]:sub(tx,tx)
				else
					output[1][y] = (output[1][y] or "").." "
				end
				output[2][y] = (output[2][y] or "")..image[2][ty]:sub(tx,tx)
				output[3][y] = (output[3][y] or "")..image[3][ty]:sub(tx,tx)
			end
		end
		if noRepeat then
			for y = 1, imageY do
				for x = 1, imageX do
					if image[1][y]:sub(x,x) ~= " " then
						tx = round(((x / imageX) * sx) - ((0.5 / imageX) * sx))
						ty = round(((y / imageY) * sy) - ((0.5 / imageY) * sx))
						output[1][ty] = stringWrite(output[1][ty], tx, image[1][y]:sub(x,x))
					end
				end
			end
		end
		return output
	end
end

nfte.stretchImageKeepAspect = function(image, sx, sy, noRepeat)
	assert(checkValid(image), "Invalid image.")
	local imX, imY = nfte.getSize(image)
	local aspect = sx / sy
	local imAspect = imX / imY
	if imAspect > aspect then
		return nfte.stretchImage(image, sx, sx / imAspect, noRepeat)
	elseif imAspect < aspect then
		return nfte.stretchImage(image, sy * imAspect, sy, noRepeat)
	else
		return nfte.stretchImage(image, sx, sy, noRepeat)
	end
end

-- will stretch and unstretch an image to radically lower its resolution
nfte.pixelateImage = function(image, amntX, amntY)
	assert(checkValid(image), "Invalid image.")
	local imageX, imageY = getSize(image)
	return stretchImage(stretchImage(image,imageX/math.max(amntX,1), imageY/math.max(amntY,1)), imageX, imageY)
end

nfte.merge = function(...)
	local images = {...}
	local output = {{},{},{}}
	local imageX, imageY = 0, 0
	local imSX, imSY
	for i = 1, #images do
		imageY = math.max(
			imageY,
			#images[i][1][1] + (images[i][3] == true and 0 or (images[i][3] - 1))
		)
		for y = 1, #images[i][1][1] do
			imageX = math.max(
				imageX,
				#images[i][1][1][y] + (images[i][2] == true and 0 or (images[i][2] - 1))
			)
		end
	end
	-- if either coordinate is true, center it
	for i = 1, #images do
		imSX, imSY = getSize(images[i][1])
		if images[i][2] == true then
			images[i][2] = round(1 + (imageX / 2) - (imSX / 2))
		end
		if images[i][3] == true then
			images[i][3] = round(1 + (imageY / 2) - (imSY / 2))
		end
	end

	-- will later add code to adjust X/Y positions if negative values are given

	local image, xadj, yadj
	local tx, ty
	for y = 1, imageY do
		output[1][y] = {}
		output[2][y] = {}
		output[3][y] = {}
		for x = 1, imageX do
			for i = #images, 1, -1 do
				image, xadj, yadj = images[i][1], images[i][2], images[i][3]
				tx, ty = x-(xadj-1), y-(yadj-1)
				output[1][y][x] = output[1][y][x] or " "
				output[2][y][x] = output[2][y][x] or " "
				output[3][y][x] = output[3][y][x] or " "
				if image[1][ty] then
					if (image[1][ty]:sub(tx,tx) ~= "") and (tx >= 1) then
						output[1][y][x] = (image[1][ty]:sub(tx,tx) == " " and output[1][y][x] or image[1][ty]:sub(tx,tx))
						output[2][y][x] = (image[2][ty]:sub(tx,tx) == " " and output[2][y][x] or image[2][ty]:sub(tx,tx))
						output[3][y][x] = (image[3][ty]:sub(tx,tx) == " " and output[3][y][x] or image[3][ty]:sub(tx,tx))
					end
				end
			end
		end
		output[1][y] = table.concat(output[1][y])
		output[2][y] = table.concat(output[2][y])
		output[3][y] = table.concat(output[3][y])
	end
	return output
end

local rotatePoint = function(x, y, angle, originX, originY)
	return
		round( (x-originX) * math.cos(angle) - (y-originY) * math.sin(angle) ) + originX,
		round( (x-originX) * math.sin(angle) + (y-originY) * math.cos(angle) ) + originY
end

nfte.rotateImage = function(image, angle, originX, originY)
	assert(checkValid(image), "Invalid image.")
	if imageX == 0 or imageY == 0 then
		return image
	end
	local output = {{},{},{}}
	local realOutput = {{},{},{}}
	local tx, ty, corners
	local imageX, imageY = getSize(image)
	local originX, originY = originX or math.floor(imageX / 2), originY or math.floor(imageY / 2)
	corners = {
		{rotatePoint(1, 		1, 		angle, originX, originY)},
		{rotatePoint(imageX, 	1, 		angle, originX, originY)},
		{rotatePoint(1, 		imageY, angle, originX, originY)},
		{rotatePoint(imageX, 	imageY, angle, originX, originY)},
	}
	local minX = math.min(corners[1][1], corners[2][1], corners[3][1], corners[4][1])
	local maxX = math.max(corners[1][1], corners[2][1], corners[3][1], corners[4][1])
	local minY = math.min(corners[1][2], corners[2][2], corners[3][2], corners[4][2])
	local maxY = math.max(corners[1][2], corners[2][2], corners[3][2], corners[4][2])

	for y = 1, (maxY - minY) + 1 do
		output[1][y] = {}
		output[2][y] = {}
		output[3][y] = {}
		for x = 1, (maxX - minX) + 1 do
			tx, ty = rotatePoint(x + minX - 1, y + minY - 1, -angle, originX, originY)
			output[1][y][x] = " "
			output[2][y][x] = " "
			output[3][y][x] = " "
			if image[1][ty] then
				if tx >= 1 and tx <= #image[1][ty] then
					output[1][y][x] = image[1][ty]:sub(tx,tx)
					output[2][y][x] = image[2][ty]:sub(tx,tx)
					output[3][y][x] = image[3][ty]:sub(tx,tx)
				end
			end
		end
	end
	for y = 1, #output[1] do
		output[1][y] = table.concat(output[1][y])
		output[2][y] = table.concat(output[2][y])
		output[3][y] = table.concat(output[3][y])
	end
	return output, math.ceil(minX), math.ceil(minY)
end









local setBarMsg = function(message)
	pain.barmsg = message
	pain.barlife = 16
	pain.doRender = true
end

local controlHoldCheck = {}	-- used to prevent repeated inputs on non-repeating controls
local control = {
	quit = {
		key = keys.q,
		holdDown = false,
		modifiers = {
			[keys.leftCtrl] = true
		},
	},
	scrollUp = { -- decrease scrollY
		key = keys.up,
		holdDown = true,
		modifiers = {},
	},
	scrollDown = {
		key = keys.down,
		holdDown = true,
		modifiers = {},
	},
	scrollLeft = {
		key = keys.left,
		holdDown = true,
		modifiers = {},
	},
	scrollRight = {
		key = keys.right,
		holdDown = true,
		modifiers = {},
	},
	resetScroll = {
		key = keys.a,
		holdDown = false,
		modifiers = {},
	},
	switchNextFrame = {
		key = keys.rightBracket,
		holdDown = false,
		modifiers = {
			[keys.leftShift] = true
		},
	},
	switchPrevFrame = {
		key = keys.leftBracket,
		holdDown = false,
		modifiers = {
			[keys.leftShift] = true
		},
	},
	swapNextFrame = {
		key = keys.rightBracket,
		holdDownn = false,
		modifiers = {
			[keys.leftShift] = true,
			[keys.leftAlt] = true,
		}
	},
	swapPrevFrame = {
		key = keys.leftBracket,
		holdDownn = false,
		modifiers = {
			[keys.leftShift] = true,
			[keys.leftAlt] = true,
		}
	},
	increaseBrushSize = {
		key = keys.equals,
		holdDown = false,
		modifiers = {},
	},
	increaseBrushSize_Alt = {
		key = keys.numPadAdd,
		holdDown = false,
		modifiers = {},
	},
	decreaseBrushSize = {
		key = keys.minus,
		holdDown = false,
		modifiers = {},
	},
	decreaseBrushSize_Alt = {
		key = keys.numPadSubtract,
		holdDown = false,
		modifiers = {},
	},
	moveMod = {
		key = keys.leftShift,
		holdDown = true,
		modifiers = {
			[keys.leftShift] = true
		},
	},
	creepMod = {
		key = keys.leftAlt,
		holdDown = true,
		modifiers = {
			[keys.leftAlt] = true
		},
	},
	toolMod = {
		key = keys.leftShift,
		holdDown = true,
		modifiers = {
			[keys.leftShift] = true
		},
	},
	pencilTool = {
		key = keys.p,
		holdDown = false,
		modifiers = {
			[keys.leftShift] = true
		},
	},
	brushTool = {
		key = keys.b,
		holdDown = false,
		modifiers = {
			[keys.leftShift] = true
		},
	},
	textTool = {
		key = keys.t,
		holdDown = false,
		modifiers = {
			[keys.leftShift] = true
		},
	},
	lineTool = {
		key = keys.l,
		holdDown = false,
		modifiers = {
			[keys.leftShift] = true
		},
	},
	adjustPaletteTextUp = {
		mouse = "mouse_scroll",
		direction = 1,
		holdDown = true,
		modifiers = {
			[keys.leftShift] = true
		},
	},
	adjustPaletteBackgroundUp = {
		mouse = "mouse_scroll",
		direction = 1,
		holdDown = true,
		modifiers = {},
	},
	adjustPaletteTextDown = {
		mouse = "mouse_scroll",
		direction = -1,
		holdDown = true,
		modifiers = {
			[keys.leftShift] = true
		},
	},
	adjustPaletteBackgroundDown = {
		mouse = "mouse_scroll",
		direction = -1,
		holdDown = true,
		modifiers = {},
	},
	adjustPaletteTextUp_Alt = {
		key = keys.rightBracket,
		modifiers = {
			[keys.leftShift] = true
		},
	},
	adjustPaletteBackgroundUp_Alt = {
		key = keys.rightBracket,
		modifiers = {},
	},
	adjustPaletteTextDown_Alt = {
		key = keys.leftBracket,
		modifiers = {
			[keys.leftShift] = true
		},
	},
	adjustPaletteBackgroundDown_Alt = {
		key = keys.leftBracket,
		modifiers = {},
	},
	selectPalette_0 = {
		key = keys.zero,
		holdDown = false,
		modifiers = {},
	},
	selectPalette_1 = {
		key = keys.one,
		holdDown = false,
		modifiers = {},
	},
	selectPalette_2 = {
		key = keys.two,
		holdDown = false,
		modifiers = {},
	},
	selectPalette_3 = {
		key = keys.three,
		holdDown = false,
		modifiers = {},
	},
	selectPalette_4 = {
		key = keys.four,
		holdDown = false,
		modifiers = {},
	},
	selectPalette_5 = {
		key = keys.five,
		holdDown = false,
		modifiers = {},
	},
	selectPalette_6 = {
		key = keys.six,
		holdDown = false,
		modifiers = {},
	},
	selectPalette_7 = {
		key = keys.seven,
		holdDown = false,
		modifiers = {},
	},
	selectPalette_8 = {
		key = keys.eight,
		holdDown = false,
		modifiers = {},
	},
	selectPalette_9 = {
		key = keys.nine,
		holdDown = false,
		modifiers = {},
	},
	selectNextPalette = {
		key = keys.rightBracket,
		holdDown = false,
		modifiers = {
			[keys.leftAlt] = true
		},
	},
	selectPrevPalette = {
		key = keys.leftBracket,
		holdDown = false,
		modifiers = {
			[keys.leftAlt] = true
		},
	},
}

local checkControl = function(name)
	local modlist = {
		keys.leftCtrl,
--		keys.rightCtrl,
		keys.leftShift,
--		keys.rightShift,
		keys.leftAlt,
--		keys.rightAlt,
	}
	for i = 1, #modlist do
		if control[name].modifiers[modlist[i]] then
			if not keysDown[modlist[i]] then
				return false
			end
		else
			if keysDown[modlist[i]] then
				return false
			end
		end
	end
	if control[name].key then
		if keysDown[control[name].key] then
			if control[name].holdDown then
				return true
			else
				if not controlHoldCheck[name] then
					controlHoldCheck[name] = true
					return true
				end
			end
		else
			controlHoldCheck[name] = false
			return false
		end
	elseif control[name].mouse then
		if miceQueue[#miceQueue] then
			if miceQueue[#miceQueue][1] == control[name].mouse and miceQueue[#miceQueue][2] == control[name].direction then
				if control[name].holdDown then
					return true
				else
					if not controlHoldCheck[name] then
						controlHoldCheck[name] = true
						return true
					end
				end
			else
				controlHoldCheck[name] = false
				return false
			end
		end
	end
end

-- converts hex colors to colors api, and back
local to_colors, to_blit = {
	[' '] = 0,
	['0'] = 1,
	['1'] = 2,
	['2'] = 4,
	['3'] = 8,
	['4'] = 16,
	['5'] = 32,
	['6'] = 64,
	['7'] = 128,
	['8'] = 256,
	['9'] = 512,
	['a'] = 1024,
	['b'] = 2048,
	['c'] = 4096,
	['d'] = 8192,
	['e'] = 16384,
	['f'] = 32768,
}, {}
for k,v in pairs(to_colors) do
	to_blit[v] = k
end

-- takes two coordinates, and returns every point between the two
local getDotsInLine = function( startX, startY, endX, endY )
	local out = {}
	startX = math.floor(startX)
	startY = math.floor(startY)
	endX = math.floor(endX)
	endY = math.floor(endY)
	if startX == endX and startY == endY then
		out = {{x=startX,y=startY}}
		return out
	end
    local minX = math.min( startX, endX )
	if minX == startX then
		minY = startY
		maxX = endX
		maxY = endY
	else
		minY = endY
		maxX = startX
		maxY = startY
	end
	local xDiff = maxX - minX
	local yDiff = maxY - minY
	if xDiff > math.abs(yDiff) then
        local y = minY
        local dy = yDiff / xDiff
        for x=minX,maxX do
            out[#out+1] = {x=x,y=math.floor(y+0.5)}
            y = y + dy
        end
    else
        local x = minX
        local dx = xDiff / yDiff
        if maxY >= minY then
            for y=minY,maxY do
                out[#out+1] = {x=math.floor(x+0.5),y=y}
                x = x + dx
            end
        else
            for y=minY,maxY,-1 do
                out[#out+1] = {x=math.floor(x+0.5),y=y}
                x = x - dx
            end
        end
    end
    return out
end

-- deletes a dot on the canvas, fool
local deleteDot = function(x, y, frame)
	x, y = 1 + x - pain.size.x, 1 + y - pain.size.y
	if canvas[frame][1][y] then
		if canvas[frame][1][y][x] then
			canvas[frame][1][y][x] = nil
			canvas[frame][2][y][x] = nil
			canvas[frame][3][y][x] = nil
		end
	end
end

-- places a dot on the canvas, predictably enough
local placeDot = function(x, y, frame, dot)
	x, y = 1 - pain.size.x + x, 1 - pain.size.y + y
	if not canvas[frame][1][y] then
		canvas[frame][1][y] = {}
		canvas[frame][2][y] = {}
		canvas[frame][3][y] = {}
	end
	canvas[frame][1][y][x] = dot[1]
	canvas[frame][2][y][x] = dot[2]
	canvas[frame][3][y][x] = dot[3]
end

-- used for tools that involve dragging
local dragPos = {}

local getGridAtPos = function(x, y)
	local guide1 = "SCREEN SIZE"
	local guide2 = "BLITTLE SIZE"
	local grid = {
		"..%%",
		"..%%",
		"..%%",
		"%%..",
		"%%..",
		"%%..",
	}
	if x < 1 or y < 1 then
		return "/", "7", "f"
	else
		if y <= pain.guideHeight then
			if x == 1 + (pain.guideWidth) or x == 1 + 2 * pain.guideWidth then
				return "@", "7", "f"
			end
		elseif y == 1 + pain.guideHeight then
			if x == 1 + 2 * pain.guideWidth then
				return "@", "7", "f"
			elseif x <= pain.guideWidth - #guide1 - pain.guideLeftAdj or (x >= pain.guideWidth - pain.guideLeftAdj + 1 and x <= 1 + pain.guideWidth) then
				return "@", "7", "f"
			elseif x <= pain.guideWidth then
				local xx = x - (pain.guideWidth - #guide1 - pain.guideLeftAdj)
				return guide1:sub(xx, xx), "7", "f"
			end
		elseif y <= pain.guideHeight * 3 then
			if x == 1 + 2 * pain.guideWidth then
				return "@", "7", "f"
			end
		elseif y == pain.guideHeight * 3 + 1 then
			if x <= 2 * pain.guideWidth - #guide2 + -pain.guideLeftAdj or (x >= 2 * pain.guideWidth - pain.guideLeftAdj + 1 and x <= 1 + 2 * pain.guideWidth) then
				return "@", "7", "f"
			elseif x <= 2 * pain.guideWidth then
				local xx = x - (2 * pain.guideWidth - #guide2 - pain.guideLeftAdj)
				return guide2:sub(xx, xx), "7", "f"
			end
		end
		local sx, sy = 1 + (1 + x) % #grid[1], 1 + (2 + y) % #grid
		return grid[sy]:sub(sx,sx), "7", "f"
	end
end

local getEvents = function(...)
	local evt
	while true do
		evt = {os.pullEvent()}
		for i = 1, #arg do
			if evt[1] == arg[i] then
				return table.unpack(evt)
			end
		end
	end
end

-- every tool at your disposal
local tools = {
	pencil = {
		info = {
			name = "Pencil",
			swapTool = "line",	-- if swap button is held, will turn into this tool
			altTool = "text",	-- if middle mouse button is held, will use this tool (overrides swapTool)
			swapArg = {			-- any values in this table will override those in 'arg' if using swapTool
				size = 1
			},
			altArg = {},		-- any values in this table will override those in 'arg' if using altTool
		},
		run = function(arg)
			if arg.event == "mouse_click" then
				if arg.actButton == 1 then
					placeDot(arg.sx, arg.sy, arg.frame, arg.dot)
				elseif arg.actButton == 2 then
					deleteDot(arg.sx, arg.sy, arg.frame)
				end
				dragPos = {arg.sx, arg.sy}
			else
				if #dragPos == 0 then
					dragPos = {arg.sx, arg.sy}
				end
				local poses = getDotsInLine(arg.sx, arg.sy, dragPos[1], dragPos[2])
				for i = 1, #poses do
					if arg.actButton == 1 then
						placeDot(poses[i].x, poses[i].y, arg.frame, arg.dot)
					elseif arg.actButton == 2 then
						deleteDot(poses[i].x, poses[i].y, arg.frame)
					end
				end
				dragPos = {arg.sx, arg.sy}
			end
		end
	},
	brush = {
		info = {
			name = "Brush",
			swapTool = "line",
			altTool = "text",
			swapArg = {},
			altArg = {}
		},
		run = function(arg)
			if arg.event == "mouse_click" then
				for y = -arg.size, arg.size do
					for x = -arg.size, arg.size do
						if math.sqrt(x^2 + y^2) <= arg.size / 2 then
							if arg.actButton == 1 then
								placeDot(arg.sx + x, arg.sy + y, arg.frame, arg.dot)
							elseif arg.actButton == 2 then
								deleteDot(arg.sx + x, arg.sy + y, arg.frame)
							end
						end
					end
				end
				dragPos = {arg.sx, arg.sy}
			else
				if #dragPos == 0 then
					dragPos = {arg.sx, arg.sy}
				end
				local poses = getDotsInLine(arg.sx, arg.sy, dragPos[1], dragPos[2])
				for i = 1, #poses do
					for y = -arg.size, arg.size do
						for x = -arg.size, arg.size do
							if math.sqrt(x^2 + y^2) <= arg.size / 2 then
								if arg.actButton == 1 then
									placeDot(poses[i].x + x, poses[i].y + y, arg.frame, arg.dot)
								elseif arg.actButton == 2 then
									deleteDot(poses[i].x + x, poses[i].y + y, arg.frame)
								end
							end
						end
					end
				end
				dragPos = {arg.sx, arg.sy}
			end
		end
	},
	text = {
		info = {
			name = "Text",
			swapTool = "pencil",
			altTool = "text",
			swapArg = {},
			altArg = {}
		},
		run = function(arg)
			pain.paused = true
			pain.barmsg = "Type text to add to canvas."
			pain.barlife = 1
			render()
			term.setCursorPos(arg.x, arg.y)
			term.setTextColor(to_colors[arg.dot[2]])
			term.setBackgroundColor(to_colors[arg.dot[3]])
			local text = read()
			-- re-render every keypress, requires custom read function
			for i = 1, #text do
				placeDot(arg.sx + i - 1, arg.sy, arg.frame, {text:sub(i,i), pain.dots[pain.dot][2], pain.dots[pain.dot][3]})
			end
			pain.paused = false
			keysDown = {}
			miceDown = {}
		end
	},
	line = {
		info = {
			name = "Line",
			swapTool = "pencil",
			altTool = "brush",
			swapArg = {},
			altArg = {}
		},
		run = function(arg)
			local dots
			while miceDown[arg.button] do
				arg.size = arg.size or pain.brushSize
				dots = getDotsInLine(
					dragPoses[arg.button][1].x + (arg.scrollX - pain.scrollX),
					dragPoses[arg.button][1].y + (arg.scrollY - pain.scrollY),
					dragPoses[arg.button][2].x,
					dragPoses[arg.button][2].y
				)
				render()
				for i = 1, #dots do
					if dots[i].x >= pain.size.x and dots[i].x < pain.size.x + pain.size.width then
						for y = -arg.size, arg.size do
							for x = -arg.size, arg.size do
								if math.sqrt(x^2 + y^2) <= arg.size / 2 then
									if (not pain.showBar) or dots[i].y + y < -1 + pain.size.y + pain.size.height then
										term.setCursorPos(dots[i].x + x, dots[i].y + y)
										if arg.actButton == 1 then
											term.blit(table.unpack(arg.dot))
										elseif arg.actButton == 2 then
											term.blit(getGridAtPos(dots[i].x + pain.scrollX + x, dots[i].y + pain.scrollY + y))
										end
									end
								end
							end
						end
					end
				end

				os.pullEvent()
			end
			-- write dots to canvas
			for i = 1, #dots do
				for y = -arg.size, arg.size do
					for x = -arg.size, arg.size do
						if math.sqrt(x^2 + y^2) <= arg.size / 2 then
							if arg.actButton == 1 then
								placeDot(dots[i].x + x + pain.scrollX, dots[i].y + y + pain.scrollY, arg.frame, arg.dot)
							elseif arg.actButton == 2 then
								deleteDot(dots[i].x + x + pain.scrollX, dots[i].y + y + pain.scrollY, arg.frame)
							end
						end
					end
				end
			end
		end
	},
}

-- ran every event on separate coroutine
-- will check if you should be using a tool given mouse and key inputs, then runs said tool
local tryTool = function()
	local swapArg = {}
	local t = tools[pain.tool]
	if miceDown[3] then
		swapArg = t.info.altArg or {}
		t = tools[t.info.altTool]
	end
	if checkControl("toolMod") then
		swapArg = t.info.swapArg or {}
		t = tools[t.info.swapTool]
	end

	swapArg.actButton = miceDown[3] and 1

	for butt = 1, 3 do
		if miceDown[butt] and t then
			t.run({
				x 			= swapArg.x or miceDown[butt].x,
				y 			= swapArg.y or miceDown[butt].y,
				sx 			= swapArg.sx or ((swapArg.x or miceDown[butt].x) + pain.scrollX),
				sy 			= swapArg.sy or ((swapArg.y or miceDown[butt].y) + pain.scrollY),
				scrollX 	= swapArg.scrollX or pain.scrollX,
				scrollY 	= swapArg.scrollY or pain.scrollY,
				frame 		= swapArg.frame or pain.frame,
				dot 		= swapArg.dot or pain.dots[pain.dot],
				size 		= swapArg.size or pain.brushSize,
				button	 	= swapArg.button or butt,
				actButton 	= swapArg.actButton or butt,	-- will act as if this button is held, if not nil
				event 		= swapArg.event or miceDown[butt].event
			})
			pain.doRender = true
			break
		end
	end
end

-- shows everything on screen
render = function(x, y, width, height)
	local buffer = {{},{},{}}
	local cx, cy
	x = x or pain.size.x
	y = y or pain.size.y
	width = width or pain.size.width
	height = height or pain.size.height
	-- see, it wouldn't do if I just individually set the cursor position for every dot
	if useDebugRenderer then

		term.clear()
		local cx, cy
		for yy, line in pairs(canvas[pain.frame][1]) do
			for xx, dot in pairs(canvas[pain.frame][1][yy]) do
				cx = xx - pain.scrollX
				cy = yy - pain.scrollY
				if cx >= x and cx <= (x + width - 1) and cy >= y and cy <= (x + width - 1) then
					term.setCursorPos(cx, cy)
					term.blit(
						canvas[pain.frame][1][yy][xx],
						canvas[pain.frame][2][yy][xx],
						canvas[pain.frame][3][yy][xx]
					)
				end
			end
		end

	else

		local gChar, gText, gBack
		for yy = 1, -1 + height + y do
			buffer[1][yy] = ""
			buffer[2][yy] = ""
			buffer[3][yy] = ""
			if pain.showBar and yy == height then
				term.setTextColor(colors.black)
				term.setBackgroundColor(colors.lightGray)
				term.setCursorPos(pain.size.x, -1 + pain.size.y + pain.size.height)
				term.write("[" .. pain.scrollX .. "," .. pain.scrollY .. "] ")
				for i = 1, #pain.dots do
					if flashPaletteOnBar > 0 then
						if i == pain.dot then
							term.blit(tostring(i), "0", pain.dots[i][3])
						else
							term.blit(tostring(i), "7", pain.dots[i][3])
						end
					else
						term.blit(table.unpack(pain.dots[i]))
					end
				end
				if pain.barlife > 0 then
					term.write(" " .. pain.barmsg)
				else
					term.write(" " .. tools[pain.tool].info.name .. " tool")
				end
				term.write((" "):rep(x + width - term.getCursorPos()))
			else
				for xx = 1, width do
					cx = xx + pain.scrollX
					cy = yy + pain.scrollY
					if canvas[pain.frame][1][cy] then
						if canvas[pain.frame][1][cy][cx] then
							for c = 1, 3 do
								buffer[c][yy] = buffer[c][yy] .. canvas[pain.frame][c][cy][cx]
							end
						else
							gChar, gText, gBack = getGridAtPos(cx, cy)
							buffer[1][yy] = buffer[1][yy] .. gChar
							buffer[2][yy] = buffer[2][yy] .. gText
							buffer[3][yy] = buffer[3][yy] .. gBack
						end
					else
						gChar, gText, gBack = getGridAtPos(cx, cy)
						buffer[1][yy] = buffer[1][yy] .. gChar
						buffer[2][yy] = buffer[2][yy] .. gText
						buffer[3][yy] = buffer[3][yy] .. gBack
					end
				end
			end
		end
		for yy = 0, height - 1 do
			term.setCursorPos(x, y + yy)
			term.blit(buffer[1][yy+1], buffer[2][yy+1], buffer[3][yy+1])
		end

	end

	if false then
		term.setCursorPos(1,1)
		write(textutils.serialize(miceDown))
	end

end

local getInput = function()
	local evt, adjX, adjY, paletteListX
	local keySwapList = {
		[keys.rightShift] = keys.leftShift,
		[keys.rightAlt] = keys.leftAlt,
		[keys.rightCtrl] = keys.leftCtrl,
	}
	while true do
		evt = {os.pullEvent()}
		if evt[1] == "mouse_click" or evt[1] == "mouse_drag" then

			-- start X for the list of color palettes to choose from
			paletteListX = 5 + #tostring(pain.scrollX) + #tostring(pain.scrollY)

			-- (x, y) relative to (pain.size.x, pain.size.y)
			adjX, adjY = 1 + evt[3] - pain.size.x, 1 + evt[4] - pain.size.y

			if adjX >= 1 and adjX <= pain.size.width and adjY >= 1 and adjY <= pain.size.height then

				pain.isInFocus = true

				if adjY == pain.size.height then

					if evt[1] == "mouse_click" then
						if adjX >= paletteListX and adjX <= -1 + paletteListX + #pain.dots then
							pain.dot = 1 + adjX - paletteListX
							setBarMsg("Selected palette " .. pain.dot .. ".")
						else
							-- openBarMenu()
						end
					end

				else

					if pain.limitOneMouseButton then
						dragPoses = {
							dragPoses[1] or {{},{}},
							dragPoses[2] or {{},{}},
							dragPoses[3] or {{},{}}
						}
						dragPoses = {
							[evt[2]] = {
								{
									x = dragPoses[evt[2]][1].x or evt[3],
									y = dragPoses[evt[2]][1].y or evt[4]
								},
								{
									x = evt[3],
									y = evt[4]
								}
							}
						}
						if evt[1] == "mouse_click" or miceDown[evt[2]] then
							miceDown = {{},{},{}}
							miceDown = {
								[evt[2]] = {
									event = evt[1],
									button = evt[2],
									x = evt[3],
									y = evt[4],
								}
							}
						end
					else
						dragPoses[evt[2]] = {
							{
								x = dragPoses[evt[2]][1].x or evt[3],
								y = dragPoses[evt[2]][1].y or evt[4]
							},
							{
								x = evt[3],
								y = evt[4]
							}
						}
						if evt[1] == "mouse_click" or miceDown[evt[2]] then
							miceDown[evt[2]] = {
								event = evt[1],
								button = evt[2],
								x = evt[3],
								y = evt[4],
							}
						end
					end

				end
			else
				pain.isInFocus = false
			end
		elseif evt[1] == "key" then
			if pain.isInFocus then
				keysDown[evt[2]] = true
				keysDown[keySwapList[evt[2]] or evt[2]] = true
			else
				keysDown = {}
			end
		elseif evt[1] == "mouse_up" then
			if pain.limitOneMouseButton then
				dragPoses = {{{},{}}, {{},{}}, {{},{}}}
			else
				dragPoses[evt[2]] = {{},{}}, {{},{}}, {{},{}}
			end
			miceDown[evt[2]] = false
		elseif evt[1] == "mouse_scroll" then
			-- capture scroll events for special use
			miceQueue[#miceQueue + 1] = evt
		elseif evt[1] == "key_up" then
			keysDown[evt[2]] = false
			keysDown[keySwapList[evt[2]] or evt[2]] = false
		end
	end
end

-- asynchronously renders the screen
local asyncRender = function()
	while true do
		os.pullEvent("pain_main_looped")
		if pain.doRender and not pain.paused then
			render()
			pain.doRender = false
		end
		sleep(0.05)
		os.queueEvent("pain_render_looped")
	end
end

-- executes everything that doesn't run asynchronously
main = function()
	local evt
	parallel.waitForAny(asyncRender, function()
		while true do

			if not pain.paused then

				if checkControl("quit") then
					return true
				end

				if checkControl("adjustPaletteTextDown") or checkControl("adjustPaletteTextDown_Alt") then
					pain.dots[pain.dot][2] = to_blit[math.max(1, to_colors[pain.dots[pain.dot][2]] / 2)]
					pain.doRender = true
				end

				if checkControl("adjustPaletteTextUp") or checkControl("adjustPaletteTextUp_Alt") then
					pain.dots[pain.dot][2] = to_blit[math.min(2^15, to_colors[pain.dots[pain.dot][2]] * 2)]
					pain.doRender = true
				end

				if checkControl("adjustPaletteBackgroundDown") or checkControl("adjustPaletteBackgroundDown_Alt") then
					pain.dots[pain.dot][3] = to_blit[math.max(1, to_colors[pain.dots[pain.dot][3]] / 2)]
					pain.doRender = true
				end

				if checkControl("adjustPaletteBackgroundUp") or checkControl("adjustPaletteBackgroundUp_Alt") then
					pain.dots[pain.dot][3] = to_blit[math.min(2^15, to_colors[pain.dots[pain.dot][3]] * 2)]
					pain.doRender = true
				end

				-- handle scrolling
				if checkControl("resetScroll") then
					pain.scrollX = 0
					pain.scrollY = 0
					pain.doRender = true
				else
					if checkControl("increaseBrushSize") or checkControl("increaseBrushSize_Alt") then
						pain.brushSize = math.min(pain.brushSize + 1, 16)
						setBarMsg("Increased brush size to " .. pain.brushSize .. ".")
					elseif checkControl("decreaseBrushSize") or checkControl("decreaseBrushSize_Alt") then
						pain.brushSize = math.max(pain.brushSize - 1, 1)
						setBarMsg("Decreased brush size to " .. pain.brushSize .. ".")
					elseif checkControl("scrollLeft") then
						pain.scrollX = pain.scrollX - 1
						pain.doRender = true
					end
					if checkControl("scrollRight") then
						pain.scrollX = pain.scrollX + 1
						pain.doRender = true
					end
					if checkControl("scrollUp") then
						pain.scrollY = pain.scrollY - 1
						pain.doRender = true
					end
					if checkControl("scrollDown") then
						pain.scrollY = pain.scrollY + 1
						pain.doRender = true
					end
				end
				if checkControl("selectNextPalette") then
					if pain.dot < #pain.dots then
						pain.dot = pain.dot + 1
						flashPaletteOnBar = 6
						setBarMsg("Switched to next palette " .. pain.dot .. ".")
					else
						setBarMsg("Reached end of palette list.")
					end
				end
				if checkControl("selectPrevPalette") then
					if pain.dot > 1 then
						pain.dot = pain.dot - 1
						flashPaletteOnBar = 6
						setBarMsg("Switched to previous palette " .. pain.dot .. ".")
					else
						setBarMsg("Reached beginning of palette list.")
					end
				end
				for i = 0, 9 do
					if checkControl("selectPalette_" .. i) then
						if pain.dots[i] then
							pain.dot = i
							flashPaletteOnBar = 6
							setBarMsg("Selected palette " .. pain.dot .. ".")
							break
						else
							setBarMsg("There is no palette " .. i .. ".")
							break
						end
					end
				end
				if checkControl("pencilTool") then
					pain.tool = "pencil"
					setBarMsg("Selected pencil tool.")
				elseif checkControl("textTool") then
					pain.tool = "text"
					setBarMsg("Selected text tool.")
				elseif checkControl("brushTool") then
					pain.tool = "brush"
					setBarMsg("Selected brush tool.")
				elseif checkControl("lineTool") then
					pain.tool = "line"
					setBarMsg("Selected line tool.")
				end

				-- decrement bar life and palette number indicator
				-- if it's gonna hit zero, make sure it re-renders

				if pain.barlife == 1 then
					pain.doRender = true
				end
				pain.barlife = math.max(pain.barlife - 1, 0)

				if flashPaletteOnBar == 1 then
					pain.doRender = true
				end
				flashPaletteOnBar = math.max(flashPaletteOnBar - 1, 0)

			end

			if #miceQueue > 0 then
				miceQueue[#miceQueue] = nil
			end

			TICKNO = TICKNO + 1

			os.queueEvent("pain_main_looped")
			os.pullEvent("pain_render_looped")

		end
	end)
end

local keepTryingTools = function()
	while true do
		os.pullEvent()
		tryTool()
	end
end

term.clear()

parallel.waitForAny( main, getInput, keepTryingTools )

-- exit cleanly

term.setCursorPos(1, scr_y)
term.setBackgroundColor(colors.black)
term.setTextColor(colors.white)
term.clearLine()
