local font = dofile(minetest.get_modpath("digistuff") .. "/gpu-font.lua")
local MAX_BUFFERS = 8

local function explodebits(input, count)
	local output = {}
	count = count or 8
	for i = 0, count - 1 do
		output[i] = input % (2^(i + 1)) >= 2^i
	end
	return output
end

local function implodebits(input, count)
	local output = 0
	count = count or 8
	for i = 0, count - 1 do
		output = output + (input[i] and 2^i or 0)
	end
	return output
end

local packtable = {}
local unpacktable = {}
for i = 0, 25 do
	packtable[i] = string.char(i + 65)
	packtable[i + 26] = string.char(i + 97)
	unpacktable[string.char(i + 65)] = i
	unpacktable[string.char(i + 97)] = i + 26
end
for i = 0, 9 do
	packtable[i + 52] = tostring(i)
	unpacktable[tostring(i)] = i + 52
end
packtable[62] = "+"
packtable[63] = "/"
unpacktable["+"] = 62
unpacktable["/"] = 63

local function packpixel(pixel)
	pixel = tonumber(pixel, 16)
	if not pixel then
		return "AAAA"
	end

	local bits = explodebits(pixel, 24)
	local block1 = {}
	local block2 = {}
	local block3 = {}
	local block4 = {}
	for i = 0, 5 do
		block1[i] = bits[i]
		block2[i] = bits[i + 6]
		block3[i] = bits[i + 12]
		block4[i] = bits[i + 18]
	end
	local char1 = packtable[implodebits(block1, 6)] or "A"
	local char2 = packtable[implodebits(block2, 6)] or "A"
	local char3 = packtable[implodebits(block3, 6)] or "A"
	local char4 = packtable[implodebits(block4, 6)] or "A"
	return char1 .. char2 .. char3 .. char4
end

local function unpackpixel(pack)
	local block1 = unpacktable[pack:sub(1, 1)] or 0
	local block2 = unpacktable[pack:sub(2, 2)] or 0
	local block3 = unpacktable[pack:sub(3, 3)] or 0
	local block4 = unpacktable[pack:sub(4, 4)] or 0
	local out = block1 + (2^6 * block2) + (2^12 * block3) + (2^18 * block4)
	return string.format("%06X", out)
end

local function rgbtohsv(r, g, b)
	r = r / 255
	g = g / 255
	b = b / 255
	local max = math.max(r, g, b)
	local min = math.min(r, g, b)
	local delta = max - min
	local hue = 0
	if delta > 0 then
		if max == r then
			hue = (g - b) / delta
			hue = (hue % 6) * 60
		elseif max == g then
			hue = (b - r) / delta
			hue = 60 * (hue + 2)
		elseif max == b then
			hue = (r - g) / delta
			hue = 60 * (hue + 4)
		end
		hue = hue / 360
	end
	local sat = 0
	if max > 0 then
		sat = delta / max
	end
	return math.floor(hue * 255), math.floor(sat * 255), math.floor(max * 255)
end

local function hsvtorgb(h, s, v)
	h = h / 255 * 360
	s = s / 255
	v = v / 255
	local c = s * v
	local x = (h / 60) % 2
	x = 1 - math.abs(x - 1)
	x = x * c
	local m = v - c
	local r = 0
	local g = 0
	local b = 0
	if h < 60 then
		r = c
		g = x
	elseif h < 120 then
		r = x
		g = c
	elseif h < 180 then
		g = c
		b = x
	elseif h < 240 then
		g = x
		b = c
	elseif h < 300 then
		r = x
		b = c
	else
		r = c
		b = x
	end
	r = r + m
	g = g + m
	b = b + m
	return math.floor(r * 255), math.floor(g * 255), math.floor(b * 255)
end

local function bitwiseblend(srcr, dstr, srcg, dstg, srcb, dstb, mode)
	local srbits = explodebits(srcr)
	local sgbits = explodebits(srcg)
	local sbbits = explodebits(srcb)
	local drbits = explodebits(dstr)
	local dgbits = explodebits(dstg)
	local dbbits = explodebits(dstb)
	for i = 0, 7 do
		if mode == "and" then
			drbits[i] = srbits[i] and drbits[i]
			dgbits[i] = sgbits[i] and dgbits[i]
			dbbits[i] = sbbits[i] and dbbits[i]
		elseif mode == "or" then
			drbits[i] = srbits[i] or drbits[i]
			dgbits[i] = sgbits[i] or dgbits[i]
			dbbits[i] = sbbits[i] or dbbits[i]
		elseif mode == "xor" then
			drbits[i] = srbits[i] ~= drbits[i]
			dgbits[i] = sgbits[i] ~= dgbits[i]
			dbbits[i] = sbbits[i] ~= dbbits[i]
		elseif mode == "xnor" then
			drbits[i] = srbits[i] == drbits[i]
			dgbits[i] = sgbits[i] == dgbits[i]
			dbbits[i] = sbbits[i] == dbbits[i]
		elseif mode == "not" then
			drbits[i] = not srbits[i]
			dgbits[i] = not sgbits[i]
			dbbits[i] = not sbbits[i]
		elseif mode == "nand" then
			drbits[i] = not (srbits[i] and drbits[i])
			dgbits[i] = not (sgbits[i] and dgbits[i])
			dbbits[i] = not (sbbits[i] and dbbits[i])
		elseif mode == "nor" then
			drbits[i] = not (srbits[i] or drbits[i])
			dgbits[i] = not (sgbits[i] or dgbits[i])
			dbbits[i] = not (sbbits[i] or dbbits[i])
		end
	end
	return string.format("%02X%02X%02X",
		implodebits(drbits), implodebits(dgbits), implodebits(dbbits))
end

local function blend(src, dst, mode, transparent)
	local srcr = tonumber(string.sub(src, 1, 2), 16)
	local srcg = tonumber(string.sub(src, 3, 4), 16)
	local srcb = tonumber(string.sub(src, 5, 6), 16)
	local dstr = tonumber(string.sub(dst, 1, 2), 16)
	local dstg = tonumber(string.sub(dst, 3, 4), 16)
	local dstb = tonumber(string.sub(dst, 5, 6), 16)
	local op = "normal"
	if type(mode) == "string" then
		op = string.lower(mode)
	end
	if op == "normal" then
		return src

	elseif op == "nop" then
		return dst

	elseif op == "overlay" then
		return string.upper(src) == string.upper(transparent) and dst or src

	elseif op == "add" then
		local r = math.min(255, srcr + dstr)
		local g = math.min(255, srcg + dstg)
		local b = math.min(255, srcb + dstb)
		return string.format("%02X%02X%02X", r, g, b)

	elseif op == "sub" then
		local r = math.max(0, dstr - srcr)
		local g = math.max(0, dstg - srcg)
		local b = math.max(0, dstb - srcb)
		return string.format("%02X%02X%02X", r, g, b)

	elseif op == "isub" then
		local r = math.max(0, srcr - dstr)
		local g = math.max(0, srcg - dstg)
		local b = math.max(0, srcb - dstb)
		return string.format("%02X%02X%02X", r, g, b)

	elseif op == "average" then
		local r = math.min(255, (srcr + dstr) / 2)
		local g = math.min(255, (srcg + dstg) / 2)
		local b = math.min(255, (srcb + dstb) / 2)
		return string.format("%02X%02X%02X", r, g, b)

	elseif op == "and"
		or op == "or"
		or op == "xor"
		or op == "xnor"
		or op == "not"
		or op == "nand"
		or op == "nor"
	then
		return bitwiseblend(srcr, dstr, srcg, dstg, srcb, dstb, op)

	elseif op == "tohsv"
		or op == "rgbtohsv"
	then
		return string.format("%02X%02X%02X", rgbtohsv(srcr, srcg, srcb))

	elseif op == "torgb"
		or op == "hsvtorgb"
	then
		return string.format("%02X%02X%02X", hsvtorgb(srcr, srcg, srcb))
	end

	return src
end

local function validate_area(buffer, x1, y1, x2, y2, keep_order)
	if not (buffer and buffer.xsize and buffer.ysize)
		or type(x1) ~= "number"
		or type(x2) ~= "number"
		or type(y1) ~= "number"
		or type(y2) ~= "number"
	then
		return
	end

	x1 = math.max(1, math.min(buffer.xsize, math.floor(x1)))
	x2 = math.max(1, math.min(buffer.xsize, math.floor(x2)))
	y1 = math.max(1, math.min(buffer.ysize, math.floor(y1)))
	y2 = math.max(1, math.min(buffer.ysize, math.floor(y2)))

	if keep_order then
		return x1, y1, x2, y2
	end

	if x1 > x2 then
		x1, x2 = x2, x1
	end
	if y1 > y2 then
		y1, y2 = y2, y1
	end
	return x1, y1, x2, y2
end

local function validate_size(size)
	if type(size) ~= "number" then
		return 1
	end
	return math.max(1, math.min(64, math.floor(math.abs(size))))
end

local function validate_color(fillcolor, fallback)
	fallback = fallback or "000000"
	if type(fillcolor) ~= "string"
		or string.len(fillcolor) > 7
		or string.len(fillcolor) < 6
	then
		fillcolor = fallback
	end
	if string.sub(fillcolor, 1, 1) == "#" then
		fillcolor = string.sub(fillcolor, 2, 7)
	end
	if not tonumber(fillcolor, 16) then
		fillcolor = fallback
	end
	return fillcolor
end

local function validate_buffer_address(bufnum)
	if type(bufnum) ~= "number" then
		return
	end

	bufnum = math.floor(math.abs(bufnum))
	return MAX_BUFFERS > bufnum and bufnum or nil
end

local function read_buffer(meta, bufnum)
	local buffer = minetest.deserialize(meta:get_string("buffer" .. bufnum))
	return type(buffer) == "table" and buffer or nil
end

local function write_buffer(meta, bufnum, buffer)
	meta:set_string("buffer" .. bufnum, minetest.serialize(buffer))
end

local function runcommand(pos, meta, command)
	if type(command) ~= "table" then
		return
	end

	local bufnum
	if command.command ~= "copy" then
		bufnum = validate_buffer_address(command.buffer)
		if not bufnum then
			return
		end
	end

	local buffer
	if command.command ~= "createbuffer" and command.command ~= "copy" then
		buffer = read_buffer(meta, bufnum)
		if not buffer then
			return
		end
	end

	local xsize, ysize, x1, x2, y1, y2
	local color, fillcolor, edgecolor
	if command.command == "createbuffer" then
		xsize = validate_size(command.xsize)
		ysize = validate_size(command.ysize)
		fillcolor = validate_color(command.fill)
		buffer = { xsize = xsize, ysize = ysize }
		for y = 1, ysize do
			buffer[y] = {}
			for x = 1, xsize do
				buffer[y][x] = fillcolor
			end
		end
		write_buffer(meta, bufnum, buffer)
	elseif command.command == "send" then
		if type(command.channel) ~= "string" then
			return
		end

		digilines.receptor_send(pos, digilines.rules.default,
			command.channel, buffer)

	elseif command.command == "sendregion" then
		if type(command.channel) ~= "string" then
			return
		end

		x1, y1, x2, y2 = validate_area(buffer,
			command.x1, command.y1, command.x2, command.y2)

		if not x1 then
			return
		end

		local tempbuf, dstx, dsty = {}
		for y = y1, y2 do
			dsty = y - y1 + 1
			tempbuf[dsty] = {}
			for x = x1, x2 do
				dstx = x - x1 + 1
				tempbuf[dsty][dstx] = buffer[y][x]
			end
		end
		digilines.receptor_send(pos, digilines.rules.default,
			command.channel, tempbuf)

	elseif command.command == "drawrect" then
		x1, y1, x2, y2 = validate_area(buffer,
			command.x1, command.y1, command.x2, command.y2)

		if not x1 then
			return
		end

		fillcolor = validate_color(command.fill)
		edgecolor = validate_color(command.edge, fillcolor)
		for y = y1, y2 do
			for x = x1, x2 do
				buffer[y][x] = fillcolor
			end
		end
		if fillcolor ~= edgecolor then
			for x = x1, x2 do
				buffer[y1][x] = edgecolor
				buffer[y2][x] = edgecolor
			end
			for y = y1, y2 do
				buffer[y][x1] = edgecolor
				buffer[y][x2] = edgecolor
			end
		end
		write_buffer(meta, bufnum, buffer)
	elseif command.command == "drawline" then
		x1, y1, x2, y2 = validate_area(buffer,
			command.x1, command.y1, command.x2, command.y2, true)

		if not x1 then
			return
		end

		color = validate_color(command.color)
		local p1 = vector.new(x1, y1, 0)
		local p2 = vector.new(x2, y2, 0)
		local length = 1 + vector.distance(p1, p2)
		local dir = vector.direction(p1, p2)
		local point
		-- Not the most efficient process for horizontal, vertical
		-- or 45 degree lines.
		for i = 0, length, 0.3 do
			point = vector.add(p1, vector.multiply(dir, i))
			point = vector.floor(point)
			-- Underflow check needed so we don't need more complicated code to
			-- calculate dir in negative direction. Overflows are already capped
			-- by initiation.
			if 1 > point.x then point.x = 1 end
			if 1 > point.y then point.y = 1 end
			if command.antialias then
				buffer[point.y][point.x] = blend(
					buffer[point.y][point.x], color, "average")
			else
				buffer[point.y][point.x] = color
			end
		end
		write_buffer(meta, bufnum, buffer)
	elseif command.command == "drawpoint" then
		x1, y1 = validate_area(buffer, command.x, command.y, command.x, command.y)
		if not x1 then
			return
		end

		buffer[y1][x1] = validate_color(command.color)
		write_buffer(meta, bufnum, buffer)
	elseif command.command == "copy" then
		if type(command.xsize) ~= "number"
			or type(command.ysize) ~= "number"
		then
			return
		end

		local src = validate_buffer_address(command.src)
		local dst = validate_buffer_address(command.dst)
		if not (src and dst) then
			return
		end

		local sourcebuffer = read_buffer(meta, src)
		local destbuffer = read_buffer(meta, dst)
		if not (sourcebuffer and destbuffer) then
			return
		end

		x1, y1 = validate_area(sourcebuffer,
			command.srcx, command.srcy, command.srcx, command.srcy)

		x2, y2 = validate_area(destbuffer,
			command.dstx, command.dsty, command.dstx, command.dsty)

		if not (x1 and x2) then
			return
		end

		-- clamp size to source and offset
		xsize = math.min(sourcebuffer.xsize - x1 + 1, validate_size(command.xsize))
		ysize = math.min(sourcebuffer.ysize - y1 + 1, validate_size(command.ysize))
		-- clamp size to destination and offset
		xsize = math.min(destbuffer.xsize - x2 + 1, xsize)
		ysize = math.min(destbuffer.ysize - y2 + 1, ysize)

		local transparent = validate_color(command.transparent)
		local px1, px2
		for y = 0, ysize - 1 do
			for x = 0, xsize - 1 do
				px1 = sourcebuffer[y1 + y][x1 + x]
				px2 = destbuffer[y2 + y][x2 + x]
				destbuffer[y2 + y][x2 + x] = blend(
					px1, px2, command.mode, transparent)
			end
		end
		write_buffer(meta, dst, destbuffer)
	elseif command.command == "load" then
		x1, y1 = validate_area(buffer, command.x, command.y, command.x, command.y)
		if not x1
			or type(command.data) ~= "table"
			or type(command.data[1]) ~= "table"
			or #command.data[1] < 1
		then
			return
		end

		ysize = math.min(buffer.ysize - y1 + 1, validate_size(#command.data))
		xsize = math.min(buffer.xsize - x1 + 1, validate_size(#command.data[1]))
		for y = 1, ysize do
			if type(command.data[y]) == "table" then
				for x = 1, xsize do
					-- slightly different behaviour from before refactor:
					-- illegal values are now set to '000000' instead of being skipped
					buffer[y1 + y - 1][x1 + x - 1] = validate_color(
						command.data[y][x])
				end
			end
		end
		write_buffer(meta, bufnum, buffer)
	elseif command.command == "text" then
		x1, y1 = validate_area(buffer, command.x, command.y, command.x, command.y)
		if not x1
			or x1 > buffer.xsize
			or y1 > buffer.ysize
			or type(command.text) ~= "string"
			or string.len(command.text) < 1
		then
			return
		end

		command.text = string.sub(command.text, 1, 16)
		color = validate_color(command.color, "ff6600")
		local char, px
		for i = 1, string.len(command.text) do
			char = font[string.byte(string.sub(command.text, i, i))]
			for chary = 1, 12 do
				for charx = 1, 5 do
					x2 = x1 + (i * 6 - 6)
					if char[chary][charx] and y1 + chary - 1 <= buffer.ysize
						and x2 + charx - 1 <= buffer.xsize
					then
						px = buffer[y1 + chary - 1][x2 + charx - 1]
						buffer[y1 + chary - 1][x2 + charx - 1] = blend(
							color, px, command.mode, "")
					end
				end
			end
		end
		write_buffer(meta, bufnum, buffer)
	elseif command.command == "sendpacked" then
		if type(command.channel) ~= "string" then
			return
		end
		local packedtable = {}
		for y = 1, buffer.ysize do
			for x = 1, buffer.xsize do
				table.insert(packedtable, packpixel(buffer[y][x]))
			end
		end
		local packeddata = table.concat(packedtable, "")
		digilines.receptor_send(pos, digilines.rules.default,
			command.channel, packeddata)
	elseif command.command == "loadpacked" then
		x1, y1 = validate_area(buffer, command.x, command.y, command.x, command.y)
		if not x1
			or type(command.data) ~= "string"
		then
			return
		end

		-- clamp size to buffer size
		xsize = math.min(buffer.xsize - x1 + 1, validate_size(command.xsize))
		ysize = math.min(buffer.ysize - y1 + 1, validate_size(command.ysize))
		local packidx, packeddata
		for y = 0, ysize - 1 do
			y2 = y1 + y
			for x = 0, xsize - 1 do
				x2 = x1 + x
				packidx = (y * xsize + x) * 4 + 1
				packeddata = string.sub(command.data, packidx, packidx + 3)
				buffer[y2][x2] = unpackpixel(packeddata)
			end
		end
		write_buffer(meta, bufnum, buffer)
	end
end

minetest.register_node("digistuff:gpu", {
	description = "Digilines 2D Graphics Processor",
	groups = { cracky = 3 },
	is_ground_content = false,
	on_construct = function(pos)
		local meta = minetest.get_meta(pos)
		meta:set_string("formspec", "field[channel;Channel;${channel}")
	end,
	tiles = {
		"digistuff_gpu_top.png",
		"jeija_microcontroller_bottom.png",
		"jeija_microcontroller_sides.png",
		"jeija_microcontroller_sides.png",
		"jeija_microcontroller_sides.png",
		"jeija_microcontroller_sides.png"
	},
	inventory_image = "digistuff_gpu_top.png",
	drawtype = "nodebox",
	selection_box = {
		--From luacontroller
		type = "fixed",
		fixed = { -8/16, -8/16, -8/16, 8/16, -5/16, 8/16 },
	},
	_digistuff_channelcopier_fieldname = "channel",
	node_box = {
		--From Luacontroller
		type = "fixed",
		fixed = {
			{ -8/16, -8/16, -8/16, 8/16, -7/16, 8/16 }, -- Bottom slab
			{ -5/16, -7/16, -5/16, 5/16, -6/16, 5/16 }, -- Circuit board
			{ -3/16, -6/16, -3/16, 3/16, -5/16, 3/16 }, -- IC
		}
	},
	paramtype = "light",
	sunlight_propagates = true,
	on_receive_fields = function(pos, formname, fields, sender)
		-- Below link to lua_api.md says: not to check formname
		-- https://github.com/minetest/minetest/blob/2efd0996e61fe82a4922224fa8c039116281d345/doc/lua_api.md?plain=1#L9674
		if not fields.channel then
			return
		end

		local name = sender:get_player_name()
		if minetest.is_protected(pos, name)
			and not minetest.check_player_privs(name, { protection_bypass = true })
		then
			minetest.record_protection_violation(pos, name)
			return
		end

		local meta = minetest.get_meta(pos)
		meta:set_string("channel", fields.channel)
	end,
	digiline = {
		receptor = {},
		effector = {
			action = function(pos, node, channel, msg)
				local meta = minetest.get_meta(pos)
				if meta:get_string("channel") ~= channel
					or type(msg) ~= "table"
				then
					return
				end

				if type(msg[1]) == "table" then
					for i = 1, 32 do
						if type(msg[i]) == "table" then
							runcommand(pos, meta, msg[i])
						end
					end
				else
					runcommand(pos, meta, msg)
				end
			end
		},
	},
})

minetest.register_craft({
	output = "digistuff:gpu",
	recipe = {
		{ "", "default:steel_ingot", "" },
		{
			"digilines:wire_std_00000000",
			"mesecons_luacontroller:luacontroller0000",
			"digilines:wire_std_00000000"
		},
		{ "dye:red", "dye:green", "dye:blue" }
	}
})

