local tchar, bchar = string.char(31), string.char(30)

local deepCopy = function(tbl)
	local output = {}
	for k,v in pairs(tbl) do
		output[k] = v
	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
			if #image[1] + #image[2] + #image[3] >= 3 then
				return (#image[1] == #image[2] and #image[2] == #image[3])
			end
		end
	end
	return false
end
local bl = {
	[' '] = 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 = {}
for k,v in pairs(bl) do
	lb[v] = k
end

local ldchart = {	--it stands for light/dark chart
	["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 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

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

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

loadImageData = function(image, background) --string image
	local output = {{},{},{}} --char, text, back
	local y = 1
	local text, back = " ", background or " "
	local doSkip, c1, c2 = false
	local maxX = 0
	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] + 1)
				y = y + 1
				text, back = " ", background or " "
			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] .. (" "):rep(maxX - #output[3][y])
	end
	return output
end

convertFromNFP = function(image)
	local output = {{},{},{}}
	local imageX, imageY = getSizeNFP(image)
	for y = 1, imageY do
		output[1][y] = ""
		output[2][y] = ""
		output[3][y] = ""
		for x = 1, imageX do
			output[1][y] = output[1][y]..lb[image[y][x] or " "]
			output[2][y] = output[2][y]..lb[image[y][x] or " "]
			output[3][y] = output[3][y]..lb[image[y][x] or " "]
		end
	end
	return output
end

loadImage = function(path, background)
	local file = fs.open(path,"r")
	local output = loadImageData(file.readAll(), background)
	file.close()
	return output
end

unloadImage = 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 + y == 2) then output = output..tchar..t end
			if (b ~= back) or (x + y == 2) then output = output..bchar..b end
			output = output..c
		end
		if y ~= #image[1] then
			output = output.."\n"
			text, back = " ", " "
		end
	end
	return output
end

drawImage = function(image, x, y)
	assert(checkValid(image), "Invalid image.")
	local cx, cy = term.getCursorPos()
	for iy = 1, #image[1] do
		term.setCursorPos(x,y+(iy-1))
		term.blit(image[1][iy], image[2][iy], image[3][iy])
	end
	term.setCursorPos(cx,cy)
end

drawImageTransparent = function(image, x, y)
	assert(checkValid(image), "Invalid image. (" .. textutils.serialize(image) .. ")")
	local cx, cy = term.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 not (b == " " and c == " ") then
				term.setCursorPos(x+(ix-1),y+(iy-1))
				term.blit(c, t, b)
			end
		end
	end
	term.setCursorPos(cx,cy)
end

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

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

colorSwap = function(image, tbl)
	local output = {{},{},{}}
	for y = 1, #image[1] do
		output[1][y] = image[1][y]
		output[2][y] = image[2][y]:gsub(".", tbl)
		output[3][y] = image[3][y]:gsub(".", tbl)
	end
	return output
end

local xflippable = {
	["\129"] = "\130",
	["\131"] = "\131",
	["\132"] = "\136",
	["\133"] = "\138",
	["\134"] = "\137",
	["\135"] = "\139",
	["\140"] = "\140",
	["\141"] = "\142",
	["\143"] = "\143",
}
local xinvertable = {
	["\144"] = "\159",
	["\145"] = "\157",
	["\146"] = "\158",
	["\147"] = "\156",
	["\148"] = "\151",
	["\152"] = "\155"
}
for k,v in pairs(xflippable) do
	xflippable[v] = k
end
for k,v in pairs(xinvertable) do
	xinvertable[v] = k
end

flipX = function(image)
	assert(checkValid(image), "Invalid image.")
	local output = {{},{},{}}
	for y = 1, #image[1] do
		output[1][y] = image[1][y]:reverse():gsub(".", xflippable):gsub(".", xinvertable)
		output[2][y] = ""
		output[3][y] = ""
		for x = 1, #image[1][y] do
			if (not xflippable[image[1][y]:sub(x,x)]) or 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

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

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 k,v in pairs(chart) do
		for y = 1, #image[1] do
			output[1][y] = image[1][y]:gsub(k,v)
			output[2][y] = image[2][y]:gsub(k,v)
			output[3][y] = image[3][y]:gsub(k,v)
		end
	end
	return output
end

greyOut = grayOut

lighten = function(image)
	assert(checkValid(image), "Invalid image.")
	local output = {{},{},{}}
	for k,v in pairs(ldchart) do
		for y = 1, #image[1] do
			output[1][y] = image[1][y]:gsub(k,v)
			output[2][y] = image[2][y]:gsub(k,v)
			output[3][y] = image[3][y]:gsub(k,v)
		end
	end
	return output
end

darken = function(image)
	assert(checkValid(image), "Invalid image.")
	local output = {{},{},{}}
	for k,v in pairs(ldchart) do
		for y = 1, #image[1] do
			output[1][y] = image[1][y]:gsub(v,k)
			output[2][y] = image[2][y]:gsub(v,k)
			output[3][y] = image[3][y]:gsub(v,k)
		end
	end
	return output
end

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
	for y = 1, sy do
		for x = 1, sx do
			tx = math.ceil((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 = math.ceil(((x / imageX) * sx) - ((0.5 / imageX) * sx))
					ty = math.ceil(((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

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

merge = function(...)
	local images = {...}
	local output = {{},{},{}}
	local imageX, imageY = 0, 0
	for i = 1, #images do
		imageY = math.max(imageY, #images[i][1][1]+(images[i][3]-1))
		for y = 1, #images[i][1][1] do
			imageX = math.max(imageX, #images[i][1][1][y]+(images[i][2]-1))
		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 = 1, #images 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

rotateImage = function(image, angle)
	local output = {{},{},{}}
	local realOutput = {{},{},{}}
	local tx, ty
	local imageX, imageY = getSize(image)
	local originX, originY = imageX / 2, imageY / 2
	local adjX, adjY = 1, 1
	for y = 1, #image[1] do
		for x = 1, #image[1][y] do
			if not (image[1][y]:sub(x,x) == " " and image[2][y]:sub(x,x) == " " and image[3][y]:sub(x,x) == " ") then
				tx = math.floor( (x-originX) * math.cos(angle) - (originY-y) * math.sin(angle) )
				ty = math.floor( (x-originX) * math.sin(angle) + (originY-y) * math.cos(angle) )
				adjX, adjY = math.min(adjX, tx), math.min(adjY, ty)
				output[1][ty] = output[1][ty] or {}
				output[2][ty] = output[2][ty] or {}
				output[3][ty] = output[3][ty] or {}
				output[1][ty][tx] = image[1][y]:sub(x,x)
				output[2][ty][tx] = image[2][y]:sub(x,x)
				output[3][ty][tx] = image[3][y]:sub(x,x)
			end
		end
	end
	for y = adjY, #output[1] do
		realOutput[1][y+1-adjY] = {}
		realOutput[2][y+1-adjY] = {}
		realOutput[3][y+1-adjY] = {}
		for x = adjX, #output[1][y] do
			realOutput[1][y+1-adjY][x+1-adjX] = output[1][y][x] or " "
			realOutput[2][y+1-adjY][x+1-adjX] = output[2][y][x] or " "
			realOutput[3][y+1-adjY][x+1-adjX] = output[3][y][x] or " "
		end
	end
	for y = 1, #realOutput[1] do
		realOutput[1][y] = table.concat(realOutput[1][y])
		realOutput[2][y] = table.concat(realOutput[2][y])
		realOutput[3][y] = table.concat(realOutput[3][y])
	end
	return realOutput, math.ceil(adjX-1+originX), math.ceil(adjY-1+originY)
end

help = function(input)
	local helpOut = {
		loadImageData = "Loads an NFT image from a string input.",
		loadImage = "Loads an NFT image from a file path.",
		convertFromNFP = "Loads a table NFP image into a table NFT image, same as what loadImage outputs.",
		drawImage = "Draws an image. Does not support transparency, sadly.",
		drawImageTransparent = "Draws an image. Supports transparency, but not as fast as drawImage.",
		drawImageCenter = "Draws an image centered around the inputted coordinates. Does not support transparency, sadly.",
		drawImageCenterTransparent = "Draws an image centered around the inputted coordinates. Supports transparency, but not as fast as drawImageCenter.",
		flipX = "Returns the inputted image, but with the X inverted.",
		flipY = "Returns the inputted image, but with the Y inverted.",
		grayOut = "Returns the inputted image, but with the colors converted into grayscale as best I could.",
		lighten = "Returns the inputted image, but with the colors lightened.",
		darken = "Returns the inputted image, but with the colors darkened.",
		stretchImage = "Returns the inputted image, but it's been stretched to the inputted size. If the fourth argument is true, it will spread non-space characters evenly in the image.",
		pixelateImage = "Returns the inputted image, but pixelated to a variable degree.",
		merge = "Merges two or more images together.",
		crop = "Crops an image between points (X1,Y1) and (X2,Y2).",
		rotateImage = "Rotates an image, and also returns how much the image center's X and Y had been adjusted.",
		colorSwap = "Swaps the colors of a given image with another color, according to an inputted table."
	}
	return helpOut[input] or "No such function."
end