--  Windon't
-- enhanced window API by LDDestroier
-- intended for general use within all me new programs
--
-- Unique features:
--  + Transparency within windows
--  + Built-in window layering

-- stores each base terminal's framebuffers to optimize rendering
local oldScreenBuffer = {}

local table_insert = table.insert
local table_concat = table.concat
local math_floor = math.floor
local to_blit = {}
local to_colors = {}

local table_compare = function(tbl1, tbl2)
	if type(tbl1) ~= "table" or type(tbl2) ~= "table" then
		return tbl1 == tbl2
	else
		for k,v in pairs(tbl1) do
			if tbl1[k] ~= tbl2[k] then
				return false
			end
		end
		for k,v in pairs(tbl2) do
			if tbl1[k] ~= tbl2[k] then
				return false
			end
		end
		return true
	end
end

local getTime = function()
	return 24 * os.day() + os.time()
end

for i = 1, 16 do
	to_blit[2 ^ (i - 1)] = ("0123456789abcdef"):sub(i, i)
	to_colors[("0123456789abcdef"):sub(i, i)] = 2 ^ (i - 1)
end
to_blit[0], to_colors["-"] = "-", 0

local nativePalette = {		-- native palette colors, since some terminals are naughty and don't contain term.nativePaletteColor()
	[ 1 ] = {
		0.94117647409439,
		0.94117647409439,
		0.94117647409439,
	},
	[ 2 ] = {
		0.94901961088181,
		0.69803923368454,
		0.20000000298023,
	},
	[ 4 ] = {
		0.89803922176361,
		0.49803921580315,
		0.84705883264542,
	},
	[ 8 ] = {
		0.60000002384186,
		0.69803923368454,
		0.94901961088181,
	},
	[ 16 ] = {
		0.87058824300766,
		0.87058824300766,
		0.42352941632271,
	},
	[ 32 ] = {
		0.49803921580315,
		0.80000001192093,
		0.098039217293262,
	},
	[ 64 ] = {
		0.94901961088181,
		0.69803923368454,
		0.80000001192093,
	},
	[ 128 ] = {
		0.29803922772408,
		0.29803922772408,
		0.29803922772408,
	},
	[ 256 ] = {
		0.60000002384186,
		0.60000002384186,
		0.60000002384186,
	},
	[ 512 ] = {
		0.29803922772408,
		0.60000002384186,
		0.69803923368454,
	},
	[ 1024 ] = {
		0.69803923368454,
		0.40000000596046,
		0.89803922176361,
	},
	[ 2048 ] = {
		0.20000000298023,
		0.40000000596046,
		0.80000001192093,
	},
	[ 4096 ] = {
		0.49803921580315,
		0.40000000596046,
		0.29803922772408,
	},
	[ 8192 ] = {
		0.34117648005486,
		0.65098041296005,
		0.30588236451149,
	},
	[ 16384 ] = {
		0.80000001192093,
		0.29803922772408,
		0.29803922772408,
	},
	[ 32768 ] = {
		0.066666670143604,
		0.066666670143604,
		0.066666670143604,
	}
}

-- list of all completely blank characters
local whitespace = {
	["\9"] = true,
	["\10"] = true,
	["\13"] = true,
	["\32"] = true,
	["\128"] = true
}

-- check if space on screenBuffer is transparent
local checkTransparent = function(buffer, x, y, blitLayer)
	if buffer[blitLayer or 1][y] then
		if blitLayer then
			return (buffer[blitLayer][y][x] and buffer[blitLayer][y][x] ~= "-")
		else
			if (not buffer[2][y][x] or buffer[2][y][x] == "-") and (not buffer[3][y][x] or buffer[3][y][x] == "-") then
				return false
			elseif (not buffer[3][y][x] or buffer[3][y][x] == "-") and (not buffer[1][y][x] or whitespace[buffer[1][y][x]]) then
				return false
			else
				return buffer[1][y][x] and buffer[2][y][x] and buffer[3][y][x]
			end
		end
	end
end

local expect = function(value, default, valueType)
	if value == nil or (valueType and type(value) ~= valueType) then
		return default
	else
		return value
	end
end

local windont = {
	doClearScreen = false,				-- if true, will clear the screen during render
	ignoreUnchangedLines = true,		-- if true, the render function will check each line it renders against the last framebuffer and ignore it if they are the same
	useSetVisible = false,				-- if true, sets the base terminal's visibility to false before rendering
	sameCharWillStencil = false,		-- if true, if one window is layered atop another and both windows have a spot where the character is the same, and the top window's text color is transparent, it will use the TEXT color of the lower window instead of the BACKGROUND color
	default = {
		baseTerm = term.current(),		-- default base terminal for all windows
		textColor = "0",				-- default text color (what " " corresponds to in term.blit's second argument)
		backColor = "f",				-- default background color (what " " corresponds to in term.blit's third argument)
		blink = true,
		visible = true,
		alwaysRender = true,			-- if true, new windows will always render if they are written to
	},
	info = {
		BLIT_CALLS = 0,				-- amount of term.blit calls during the last render
		LAST_RENDER_TIME = 0,		-- last time in which render was called
		LAST_RENDER_AMOUNT = 0,		-- amount of windows drawn during last render
		LAST_RENDER_WINDOWS = {},	-- table of the last window objects that were rendered
	}
}

-- draws one or more windon't objects
-- should not draw over any terminal space that isn't occupied by a window

windont.render = function(options, ...)
--	potential options:
--		number:		onlyX1
--		number:		onlyX2
--		number:		onlyY
--		boolean:	force		(forces render / ignores optimiztaion that compares current framebuffer to old one)
--		terminal:	baseTerm	(forces to render onto this terminal instead of the window's base terminal)

	local windows = {...}
	options = options or {}
	local bT, scr_x, scr_y

	-- checks if "options" is actually the first window, just in case
	if type(options.meta) == "table" then
		if (
			type(options.meta.buffer) == "table" and
			type(options.meta.x) == "number" and
			type(options.meta.y) == "number" and
			type(options.meta.newBuffer) == "function"
		) then
			table_insert(windows, 1, options)
		end
	end

	local screenBuffer = {{}, {}, {}}
	local blitList = {}	-- list of blit commands per line
	local c	= 1 		-- current blitList entry

	local cTime = getTime()

	local AMNT_OF_BLITS = 0	-- how many blit calls are there?

	local cx, cy							-- each window's absolute X and Y
	local char_cx, text_cx, back_cx			-- each window's transformed absolute X's in table form
	local char_cy, text_cy, back_cy			-- each window's transformed absolute X's in table form
	local buffer							-- each window's buffer
	local newChar, newText, newBack			-- if the transformation function declares a new dot, this is it
	local oriChar, oriText, oriBack
	local char_out, text_out, back_out		-- three tables, directly returned from the transformation functions

	local baseTerms = {}
	if type(options.baseTerm) == "table" then
		for i = 1, #windows do
			baseTerms[options.baseTerm] = baseTerms[options.baseTerm] or {}
			baseTerms[options.baseTerm][i] = true
		end
	else
		for i = 1, #windows do
			baseTerms[windows[i].meta.baseTerm] = baseTerms[windows[i].meta.baseTerm] or {}
			baseTerms[windows[i].meta.baseTerm][i] = true
		end
	end

	for bT, bT_list in pairs(baseTerms) do
		if bT == output then
			bT = options.baseTerm or output.meta.baseTerm
		end
		if windont.useSetVisible and bT.setVisible then
			bT.setVisible(false)
		end
		scr_x, scr_y = bT.getSize()
		-- try entire buffer transformations
		for i = #windows, 1, -1 do
			if bT_list[i] then
				if windows[i].meta.metaTransformation then
					-- metaTransformation functions needn't return a value
					windows[i].meta.metaTransformation(windows[i].meta)
				end
			end
		end
		for y = options.onlyY or 1, options.onlyY or scr_y do
			screenBuffer[1][y] = {}
			screenBuffer[2][y] = {}
			screenBuffer[3][y] = {}
			blitList = {}
			c = 1
			for x = options.onlyX1 or 1, math.min(scr_x, options.onlyX2 or scr_x) do
				for i = #windows, 1, -1 do
					if bT_list[i] then
						newChar, newText, newBack = nil
						if windows[i].meta.visible then
							buffer = windows[i].meta.buffer

							cx = 1 + x + -windows[i].meta.x
							cy = 1 + y + -windows[i].meta.y
							char_cx, text_cx, back_cx = cx, cx, cx
							char_cy, text_cy, back_cy = cy, cy, cy

							oriChar = (buffer[1][cy] or {})[cx]
							oriText = (buffer[2][cy] or {})[cx]
							oriBack = (buffer[3][cy] or {})[cx]

							-- try transformation
							if windows[i].meta.transformation then
								char_out, text_out, back_out = windows[i].meta.transformation(cx, cy, oriChar, oriText, oriBack, windows[i].meta)

								if char_out then
									char_cx = math_floor(char_out[1] or cx)
									char_cy = math_floor(char_out[2] or cy)
									if (char_out[1] % 1 ~= 0) or (char_out[2] % 1 ~= 0) then
										newChar = " "
									else
										newChar = char_out[3]
									end
								end

								if text_out then
									text_cx = math_floor(text_out[1] or cx)
									text_cy = math_floor(text_out[2] or cy)
									newText = text_out[3]
								end

								if back_out then
									back_cx = math_floor(back_out[1] or cx)
									back_cy = math_floor(back_out[2] or cy)
									newBack = back_out[3]
								end
							end

							if checkTransparent(buffer, char_cx, char_cy) or checkTransparent(buffer, text_cx, text_cy) or checkTransparent(buffer, back_cx, back_cy) then

								screenBuffer[2][y][x] = newText or checkTransparent(buffer, text_cx, text_cy, 2) and (buffer[2][text_cy][text_cx]) or (
									(buffer[1][text_cy][text_cx] == screenBuffer[1][y][x]) and (windont.sameCharWillStencil) and
										screenBuffer[2][y][x]
									or
										screenBuffer[3][y][x]
									)
								screenBuffer[1][y][x] = newChar or checkTransparent(buffer, char_cx, char_cy   ) and (buffer[1][char_cy][char_cx]) or screenBuffer[1][y][x]
								screenBuffer[3][y][x] = newBack or checkTransparent(buffer, back_cx, back_cy, 3) and (buffer[3][back_cy][back_cx]) or screenBuffer[3][y][x]
							end
						end
					end
				end

				if windont.doClearScreen then
					screenBuffer[1][y][x] = screenBuffer[1][y][x] or " "
				end
				screenBuffer[2][y][x] = screenBuffer[2][y][x] or windont.default.backColor	-- intentionally not the default text color
				screenBuffer[3][y][x] = screenBuffer[3][y][x] or windont.default.backColor

				if checkTransparent(screenBuffer, x, y) then
					if checkTransparent(screenBuffer, -1 + x, y) then
						blitList[c][1] = blitList[c][1] .. screenBuffer[1][y][x]
						blitList[c][2] = blitList[c][2] .. screenBuffer[2][y][x]
						blitList[c][3] = blitList[c][3] .. screenBuffer[3][y][x]
					else
						c = x
						blitList[c] = {
							screenBuffer[1][y][x],
							screenBuffer[2][y][x],
							screenBuffer[3][y][x]
						}
					end
				end
			end
			if (not oldScreenBuffer[bT]) or (not windont.ignoreUnchangedLines) or (options.force) or (
				(not table_compare(screenBuffer[1][y], oldScreenBuffer[bT][1][y])) or
				(not table_compare(screenBuffer[2][y], oldScreenBuffer[bT][2][y])) or
				(not table_compare(screenBuffer[3][y], oldScreenBuffer[bT][3][y]))
			) then
				for k,v in pairs(blitList) do
					bT.setCursorPos(k, y)
					bT.blit(v[1], v[2], v[3])
					AMNT_OF_BLITS = 1 + AMNT_OF_BLITS
				end
			end
		end
		oldScreenBuffer[bT] = screenBuffer
		if windont.useSetVisible and bT.setVisible then
			if not multishell then
				bT.setVisible(true)
			elseif multishell.getFocus() == multishell.getCurrent() then
				bT.setVisible(true)
			end
		end
	end

	windont.info.LAST_RENDER_AMOUNT = #windows
	windont.info.BLIT_CALLS = AMNT_OF_BLITS
	windont.info.LAST_RENDER_WINDOWS = windows
	windont.info.LAST_RENDER_TIME = cTime
	windont.info.LAST_RENDER_DURATION = getTime() + -cTime

end

-- creates a new windon't object that can be manipulated the same as a regular window

windont.newWindow = function( x, y, width, height, misc )

	-- check argument types
	assert(type(x) == "number", "argument #1 must be number, got " .. type(x))
	assert(type(y) == "number", "argument #2 must be number, got " .. type(y))
	assert(type(width) == "number", "argument #3 must be number, got " .. type(width))
	assert(type(height) == "number", "argument #4 must be number, got " .. type(height))

	-- check argument validity
	assert(x > 0, "x position must be above zero")
	assert(y > 0, "y position must be above zero")
	assert(width > 0, "width must be above zero")
	assert(height > 0, "height must be above zero")

	local output = {}
	misc = misc or {}
	local meta = {
		x 				= expect(x, 1),												-- x position of the window
		y 				= expect(y, 1),												-- y position of the window
		width 			= width,															-- width of the buffer
		height			= height,															-- height of the buffer
		buffer 			= expect(misc.buffer, {}, "table"),							-- stores contents of terminal in buffer[1][y][x] format
		renderBuddies 	= expect(misc.renderBuddies, {}, "table"),						-- renders any other window objects stored here after rendering here
		baseTerm 		= expect(misc.baseTerm, windont.default.baseTerm, "table"),	-- base terminal for which this window draws on
		isColor 		= expect(misc.isColor, term.isColor(), "boolean"),				-- if true, then it's an advanced computer

		transformation 	= expect(misc.transformation, nil, "function"),			-- function that transforms the char/text/back dots of the window
		metaTransformation = expect(misc.metaTransformation, nil, "function"),			-- function that transforms the whole output.meta function

		cursorX 		= expect(misc.cursorX, 1),
		cursorY 		= expect(misc.cursorY, 1),

		textColor 		= expect(misc.textColor, windont.default.textColor, "string"),			-- current text color
		backColor 		= expect(misc.backColor, windont.default.backColor, "string"),			-- current background color

		blink 			= expect(misc.blink, windont.default.blink, "boolean"),				-- cursor blink
		alwaysRender 	= expect(misc.alwaysRender, windont.default.alwaysRender, "boolean"),	-- render after every terminal operation
		visible 		= expect(misc.visible, windont.default.visible, "boolean"),			-- if false, don't render ever

		-- make a new buffer (optionally uses an existing buffer as a reference)
		newBuffer = function(width, height, char, text, back, drawAtop)
			local output = {{}, {}, {}}
			drawAtop = drawAtop or {{}, {}, {}}
			for y = 1, height do
				output[1][y] = output[1][y] or {}
				output[2][y] = output[2][y] or {}
				output[3][y] = output[3][y] or {}
				for x = 1, width do
					output[1][y][x] = (drawAtop[1][y] or {})[x] or (output[1][y][x] or (char or " "))
					output[2][y][x] = (drawAtop[2][y] or {})[x] or (output[2][y][x] or (text or "0"))
					output[3][y][x] = (drawAtop[3][y] or {})[x] or (output[3][y][x] or (back or "f"))
				end
			end
			return output
		end
	}

	bT = meta.baseTerm

	-- initialize the buffer
	meta.buffer = meta.newBuffer(meta.width, meta.height, " ", meta.textColor, meta.backColor)

	output.meta = meta

	output.write = function(text)
		assert(type(text) == "string" or type(text) == "number", "expected string, got " .. type(text))
		local initX = meta.cursorX
		for i = 1, #tostring(text) do
			if meta.cursorX >= 1 and meta.cursorX <= meta.width and meta.cursorY >= 1 and meta.cursorY <= meta.height then
				if not meta.buffer[1] then
					error("what the fuck happened")
				end
				meta.buffer[1][meta.cursorY][meta.cursorX] = tostring(text):sub(i,i)
				meta.buffer[2][meta.cursorY][meta.cursorX] = meta.textColor
				meta.buffer[3][meta.cursorY][meta.cursorX] = meta.backColor
			end
			meta.cursorX = meta.cursorX + 1
		end
		if meta.alwaysRender then
			output.redraw(
				-1 + meta.x + initX,
				-1 + meta.x + meta.cursorX,
				-1 + meta.y + meta.cursorY
			)
		end
	end

	output.blit = function(char, text, back)
		assert(type(char) == "string" and type(text) == "string" and type(back) == "string", "all arguments must be strings")
		assert(#char == #text and #text == #back, "arguments must be same length")
		local initX = meta.cursorX
		for i = 1, #char do
			if meta.cursorX >= 1 and meta.cursorX <= meta.width and meta.cursorY >= 1 and meta.cursorY <= meta.height then
				meta.buffer[1][meta.cursorY][meta.cursorX] = char:sub(i,i)
				meta.buffer[2][meta.cursorY][meta.cursorX] = to_colors[text:sub(i,i)] and windont.default.textColor or text:sub(i,i)
				meta.buffer[3][meta.cursorY][meta.cursorX] = to_colors[back:sub(i,i)] and windont.default.backColor or back:sub(i,i)
				meta.cursorX = meta.cursorX + 1
			end
		end
		if meta.alwaysRender then
			output.redraw(
				-1 + meta.x + initX,
				-1 + meta.x + meta.cursorX,
				-1 + meta.y + meta.cursorY
			)
		end
	end

	output.setCursorPos = function(x, y)
		assert(type(x) == "number", "argument #1 must be number, got " .. type(x))
		assert(type(y) == "number", "argument #2 must be number, got " .. type(y))
		meta.cursorX, meta.cursorY = math.floor(x), math.floor(y)
		if meta.alwaysRender then
			if bT == output then
				bT = output.meta.baseTerm
			end
			bT.setCursorPos(
				-1 + meta.x + meta.cursorX,
				-1 + meta.y + meta.cursorY
			)
		end
	end

	output.getCursorPos = function()
		return meta.cursorX, meta.cursorY
	end

	output.setTextColor = function(color)
		if to_blit[color] then
			meta.textColor = to_blit[color]
		else
			error("Invalid color (got " .. color .. ")")
		end
	end
	output.setTextColour = output.setTextColor

	output.setBackgroundColor = function(color)
		if to_blit[color] then
			meta.backColor = to_blit[color]
		else
			error("Invalid color (got " .. color .. ")")
		end
	end
	output.setBackgroundColour = output.setBackgroundColor

	output.getTextColor = function()
		return to_colors[meta.textColor]
	end
	output.getTextColour = output.getTextColor

	output.getBackgroundColor = function()
		return to_colors[meta.backColor]
	end
	output.getBackgroundColour = output.getBackgroundColor

	output.setVisible = function(visible)
		assert(type(visible) == "boolean", "bad argument #1 (expected boolean, got " .. type(visible) .. ")")
		meta.visible = visible and true or false
	end

	output.clear = function()
		meta.buffer = meta.newBuffer(meta.width, meta.height, " ", meta.textColor, meta.backColor)
		if meta.alwaysRender then
			output.redraw()
		end
	end

	output.clearLine = function()
		meta.buffer[1][meta.cursorY] = nil
		meta.buffer[2][meta.cursorY] = nil
		meta.buffer[3][meta.cursorY] = nil
		meta.buffer = meta.newBuffer(meta.width, meta.height, " ", meta.textColor, meta.backColor, meta.buffer)
		if meta.alwaysRender then
			bT.setCursorPos(meta.x, -1 + meta.y + meta.cursorY)
			bT.blit(
				(" "):rep(meta.width),
				(meta.textColor):rep(meta.width),
				(meta.backColor):rep(meta.width)
			)
		end
	end

	output.getLine = function(y)
		assert(type(y) == "number", "bad argument #1 (expected number, got " .. type(y) .. ")")
		assert(meta.buffer[1][y], "Line is out of range.")
		return table_concat(meta.buffer[1][y]), table_concat(meta.buffer[2][y]), table_concat(meta.buffer[3][y])
	end

	output.scroll = function(amplitude)
		if math.abs(amplitude) < meta.height then	-- minor optimization
			local blank = {{}, {}, {}}
			for x = 1, meta.width do
				blank[1][x] = " "
				blank[2][x] = meta.textColor
				blank[3][x] = meta.backColor
			end
			for y = 1, meta.height do
				meta.buffer[1][y] = meta.buffer[1][y + amplitude] or blank[1]
				meta.buffer[2][y] = meta.buffer[2][y + amplitude] or blank[2]
				meta.buffer[3][y] = meta.buffer[3][y + amplitude] or blank[3]
			end
		else
			meta.buffer = meta.newBuffer(meta.width, meta.height, " ", meta.textColor, meta.backColor)
		end
		if meta.alwaysRender then
			if math_floor(amplitude) ~= 0 then
				output.redraw()
			end
		end
	end

	output.getSize = function()
		return meta.width, meta.height
	end

	output.isColor = function()
		return meta.isColor
	end
	output.isColour = output.isColor

	output.reposition = function(x, y, width, height)
		assert(type(x) == "number", "bad argument #1 (expected number, got " .. type(x) .. ")")
		assert(type(y) == "number", "bad argument #2 (expected number, got " .. type(y) .. ")")
		meta.x = math_floor(x)
		meta.y = math_floor(y)
		if width then
			assert(type(width) == "number", "bad argument #3 (expected number, got " .. type(width) .. ")")
			assert(type(height) == "number", "bad argument #4 (expected number, got " .. type(height) .. ")")
			meta.width = width
			meta.height = height
			meta.buffer = meta.newBuffer(meta.width, meta.height, " ", meta.textColor, meta.backColor, meta.buffer)
		end
		if meta.alwaysRender then
			output.redraw()
		end
	end

	output.restoreCursor = function()
		bT.setCursorPos(
			math.max(0, -1 + meta.x + meta.cursorX),
			math.max(0, -1 + meta.y + meta.cursorY)
		)
		bT.setCursorBlink(meta.blink)
	end

	output.getPosition = function()
		return meta.x, meta.y
	end

	output.setCursorBlink = function(blink)
		meta.blink = blink and true or false
	end

	output.getCursorBlink = function(blink)
		return meta.blink
	end

	output.setPaletteColor = bT.setPaletteColor
	output.setPaletteColour = bT.setPaletteColour
	output.getPaletteColor = bT.getPaletteColor
	output.getPaletteColour = bT.getPaletteColour

	if bT.getPaletteColor then
		output.nativePaletteColor = bT.nativePaletteColor or function(col)
			if nativePalette[col] then
				return table.unpack(nativePalette[col])
			else
				return table.unpack(nativePalette[1]) -- I don't get how this function takes in non-base2 numbers...
			end
		end
	end

	output.redraw = function(x1, x2, y, options)
		options = options or {}
		options.onlyX1 = x1
		options.onlyX2 = x2
		options.onlyY = y
		if #meta.renderBuddies > 0 then
			windont.render(options, output, table.unpack(meta.renderBuddies))
		else
			windont.render(options, output)
		end
		output.restoreCursor()
	end

	if meta.alwaysRender then
		output.redraw()
	end

	return output

end

return windont
