
local color = require("Color")
local filesystem = require("Filesystem")

--------------------------------------------------------------------------------

local image = {}

local OCIFSignature = "OCIF"
local encodingMethodsLoad = {}
local encodingMethodsSave = {}

--------------------------------------------------------------------------------

local function group(picture, compressColors)
	local groupedPicture, x, y, background, foreground = {}, 1, 1

	for i = 3, #picture, 4 do
		if compressColors then
			background, foreground = color.to8Bit(picture[i]), color.to8Bit(picture[i + 1])

			if i % 603 == 0 then
				computer.pullSignal(0)
			end
		else
			background, foreground = picture[i], picture[i + 1]
		end

		groupedPicture[picture[i + 2]] = groupedPicture[picture[i + 2]] or {}
		groupedPicture[picture[i + 2]][picture[i + 3]] = groupedPicture[picture[i + 2]][picture[i + 3]] or {}
		groupedPicture[picture[i + 2]][picture[i + 3]][background] = groupedPicture[picture[i + 2]][picture[i + 3]][background] or {}
		groupedPicture[picture[i + 2]][picture[i + 3]][background][foreground] = groupedPicture[picture[i + 2]][picture[i + 3]][background][foreground] or {}
		groupedPicture[picture[i + 2]][picture[i + 3]][background][foreground][y] = groupedPicture[picture[i + 2]][picture[i + 3]][background][foreground][y] or {}

		table.insert(groupedPicture[picture[i + 2]][picture[i + 3]][background][foreground][y], x)

		x = x + 1

		if x > picture[1] then
			x, y = 1, y + 1
		end
	end

	return groupedPicture
end

encodingMethodsSave[5] = function(file, picture)
	file:writeBytes(
		bit32.rshift(picture[1], 8),
		bit32.band(picture[1], 0xFF)
	)

	file:writeBytes(
		bit32.rshift(picture[2], 8),
		bit32.band(picture[2], 0xFF)
	)

	for i = 3, #picture, 4 do
		file:writeBytes(
			color.to8Bit(picture[i]),
			color.to8Bit(picture[i + 1]),
			math.floor(picture[i + 2] * 255)
		)

		file:write(picture[i + 3])
	end
end

encodingMethodsLoad[5] = function(file, picture)
	picture[1] = file:readBytes(2)
	picture[2] = file:readBytes(2)

	for i = 1, image.getWidth(picture) * image.getHeight(picture) do
		table.insert(picture, color.to24Bit(file:readBytes(1)))
		table.insert(picture, color.to24Bit(file:readBytes(1)))
		table.insert(picture, file:readBytes(1) / 255)
		table.insert(picture, file:readUnicodeChar())
	end
end

local function loadOCIF67(file, picture, mode)
	picture[1] = file:readBytes(1)
	picture[2] = file:readBytes(1)

	local currentAlpha, currentSymbol, currentBackground, currentForeground, currentY

	for alpha = 1, file:readBytes(1) + mode do
		currentAlpha = file:readBytes(1) / 255
		
		for symbol = 1, file:readBytes(2) + mode do
			currentSymbol = file:readUnicodeChar()
			
			for background = 1, file:readBytes(1) + mode do
				currentBackground = color.to24Bit(file:readBytes(1))
				
				for foreground = 1, file:readBytes(1) + mode do
					currentForeground = color.to24Bit(file:readBytes(1))
					
					for y = 1, file:readBytes(1) + mode do
						currentY = file:readBytes(1)
						
						for x = 1, file:readBytes(1) + mode do
							image.set(
								picture,
								file:readBytes(1),
								currentY,
								currentBackground,
								currentForeground,
								currentAlpha,
								currentSymbol
							)
						end
					end
				end
			end
		end
	end
end

local function saveOCIF67(file, picture, mode)
	local function getGroupSize(t)
		local size = mode == 1 and -1 or 0
		
		for key in pairs(t) do
			size = size + 1
		end
    
		return size
	end
	
	-- Grouping picture by it's alphas, symbols and colors
	local groupedPicture = group(picture, true)

	-- Writing 1 byte per image width and height
	file:writeBytes(
		picture[1],
		picture[2]
	)

	-- Writing 1 byte for alphas array size
	file:writeBytes(getGroupSize(groupedPicture))

	local symbolsSize

	for alpha in pairs(groupedPicture) do
		symbolsSize = getGroupSize(groupedPicture[alpha])

		file:writeBytes(
			-- Writing 1 byte for current alpha value
			math.floor(alpha * 255),
			-- Writing 2 bytes for symbols array size
			bit32.rshift(symbolsSize, 8),
			bit32.band(symbolsSize, 0xFF)
		)

		for symbol in pairs(groupedPicture[alpha]) do
			-- Writing current unicode symbol value
			file:write(symbol)
			-- Writing 1 byte for backgrounds array size
			file:writeBytes(getGroupSize(groupedPicture[alpha][symbol]))

			for background in pairs(groupedPicture[alpha][symbol]) do
				file:writeBytes(
					-- Writing 1 byte for background color value (compressed by color)
					background,
					-- Writing 1 byte for foregrounds array size
					getGroupSize(groupedPicture[alpha][symbol][background])
				)

				for foreground in pairs(groupedPicture[alpha][symbol][background]) do
					file:writeBytes(
						-- Writing 1 byte for foreground color value (compressed by color)
						foreground,
						-- Writing 1 byte for y array size
						getGroupSize(groupedPicture[alpha][symbol][background][foreground])
					)
					
					for y in pairs(groupedPicture[alpha][symbol][background][foreground]) do
						file:writeBytes(
							-- Writing 1 byte for current y value
							y,
							-- Writing 1 byte for x array size
							#groupedPicture[alpha][symbol][background][foreground][y] - mode
						)

						for x = 1, #groupedPicture[alpha][symbol][background][foreground][y] do
							file:writeBytes(groupedPicture[alpha][symbol][background][foreground][y][x])
						end
					end
				end
			end
		end
	end
end

encodingMethodsSave[6] = function(file, picture)
	saveOCIF67(file, picture, 0)
end

encodingMethodsLoad[6] = function(file, picture)
	loadOCIF67(file, picture, 0)
end

encodingMethodsSave[7] = function(file, picture)
	saveOCIF67(file, picture, 1)
end

encodingMethodsLoad[7] = function(file, picture)
	loadOCIF67(file, picture, 1)
end

--------------------------------------------------------------------------------

function image.getIndex(x, y, width)
	return 4 * (width * (y - 1) + x) - 1
end

function image.create(width, height, background, foreground, alpha, symbol, random)
	local picture = {width, height}

	for i = 1, width * height do
		table.insert(picture, random and math.random(0x0, 0xFFFFFF) or (background or 0x0))
		table.insert(picture, random and math.random(0x0, 0xFFFFFF) or (foreground or 0x0))
		table.insert(picture, alpha or 0x0)
		table.insert(picture, random and string.char(math.random(65, 90)) or (symbol or " "))
	end

	return picture
end

function image.copy(picture)
	local newPicture = {}
	
	for i = 1, #picture do
		newPicture[i] = picture[i]
	end

	return newPicture
end

function image.save(path, picture, encodingMethod)
	encodingMethod = encodingMethod or 6
	
	local file, reason = filesystem.open(path, "wb")
	if file then	
		if encodingMethodsSave[encodingMethod] then
			file:write(OCIFSignature, string.char(encodingMethod))

			local result, reason = xpcall(encodingMethodsSave[encodingMethod], debug.traceback, file, picture)
			
			file:close()

			if result then
				return true
			else
				return false, "Failed to save OCIF image: " .. tostring(reason)
			end
		else
			file:close()
			return false, "Failed to save OCIF image: encoding method \"" .. tostring(encodingMethod) .. "\" is not supported"
		end
	else
		return false, "Failed to open file for writing: " .. tostring(reason)
	end
end

function image.load(path)
	local file, reason = filesystem.open(path, "rb")
	if file then
		local readedSignature = file:readString(#OCIFSignature)
		if readedSignature == OCIFSignature then
			local encodingMethod = file:readBytes(1)
			if encodingMethodsLoad[encodingMethod] then
				local picture = {}
				local result, reason = xpcall(encodingMethodsLoad[encodingMethod], debug.traceback, file, picture)
				
				file:close()

				if result then
					return picture
				else
					return false, "Failed to load OCIF image: " .. tostring(reason)
				end
			else
				file:close()
				return false, "Failed to load OCIF image: encoding method \"" .. tostring(encodingMethod) .. "\" is not supported"
			end
		else
			file:close()
			return false, "Failed to load OCIF image: binary signature \"" .. tostring(readedSignature) .. "\" is not valid"
		end
	else
		return false, "Failed to open file \"" .. tostring(path) .. "\" for reading: " .. tostring(reason)
	end
end

-------------------------------------------------------------------------------

function image.toString(picture)
	local charArray = {
		string.format("%02X", picture[1]),
		string.format("%02X", picture[2])
	}
	
	for i = 3, #picture, 4 do
		table.insert(charArray, string.format("%02X", color.to8Bit(picture[i])))
		table.insert(charArray, string.format("%02X", color.to8Bit(picture[i + 1])))
		table.insert(charArray, string.format("%02X", math.floor(picture[i + 2] * 255)))
		table.insert(charArray, picture[i + 3])

		if i % 603 == 0 then
			computer.pullSignal(0)
		end
	end

	return table.concat(charArray)
end

function image.fromString(pictureString)
	local picture = {
		tonumber("0x" .. unicode.sub(pictureString, 1, 2)),
		tonumber("0x" .. unicode.sub(pictureString, 3, 4)),
	}

	for i = 5, unicode.len(pictureString), 7 do
		table.insert(picture, color.to24Bit(tonumber("0x" .. unicode.sub(pictureString, i, i + 1))))
		table.insert(picture, color.to24Bit(tonumber("0x" .. unicode.sub(pictureString, i + 2, i + 3))))
		table.insert(picture, tonumber("0x" .. unicode.sub(pictureString, i + 4, i + 5)) / 255)
		table.insert(picture, unicode.sub(pictureString, i + 6, i + 6))
	end

	return picture
end

--------------------------------------------------------------------------------

function image.set(picture, x, y, background, foreground, alpha, symbol)
	local index = image.getIndex(x, y, picture[1])
	picture[index], picture[index + 1], picture[index + 2], picture[index + 3] = background, foreground, alpha, symbol

	return picture
end

function image.get(picture, x, y)
	local index = image.getIndex(x, y, picture[1])
	return picture[index], picture[index + 1], picture[index + 2], picture[index + 3]
end

function image.getSize(picture)
	return picture[1], picture[2]
end

function image.getWidth(picture)
	return picture[1]
end

function image.getHeight(picture)
	return picture[2]
end

function image.transform(picture, newWidth, newHeight)
	local newPicture, stepWidth, stepHeight, background, foreground, alpha, symbol = {newWidth, newHeight}, picture[1] / newWidth, picture[2] / newHeight
	
	local x, y = 1, 1
	for j = 1, newHeight do
		for i = 1, newWidth do
			background, foreground, alpha, symbol = image.get(picture, math.floor(x), math.floor(y))
			table.insert(newPicture, background)
			table.insert(newPicture, foreground)
			table.insert(newPicture, alpha)
			table.insert(newPicture, symbol)

			x = x + stepWidth
		end

		x, y = 1, y + stepHeight
	end

	return newPicture
end

function image.crop(picture, fromX, fromY, width, height)
	if fromX >= 1 and fromY >= 1 and fromX + width - 1 <= picture[1] and fromY + height - 1 <= picture[2] then
		local newPicture, background, foreground, alpha, symbol = {width, height}
		
		for y = fromY, fromY + height - 1 do
			for x = fromX, fromX + width - 1 do
				background, foreground, alpha, symbol = image.get(picture, x, y)
				table.insert(newPicture, background)
				table.insert(newPicture, foreground)
				table.insert(newPicture, alpha)
				table.insert(newPicture, symbol)
			end
		end

		return newPicture
	else
		return false, "Failed to crop image: target coordinates are out of source range"
	end
end

function image.flipHorizontally(picture)
	local newPicture, background, foreground, alpha, symbol = {picture[1], picture[2]}
	
	for y = 1, picture[2] do
		for x = picture[1], 1, -1 do
			background, foreground, alpha, symbol = image.get(picture, x, y)
			table.insert(newPicture, background)
			table.insert(newPicture, foreground)
			table.insert(newPicture, alpha)
			table.insert(newPicture, symbol)
		end
	end

	return newPicture
end

function image.flipVertically(picture)
	local newPicture, background, foreground, alpha, symbol = {picture[1], picture[2]}
	
	for y = picture[2], 1, -1 do
		for x = 1, picture[1] do
			background, foreground, alpha, symbol = image.get(picture, x, y)
			table.insert(newPicture, background)
			table.insert(newPicture, foreground)
			table.insert(newPicture, alpha)
			table.insert(newPicture, symbol)
		end
	end

	return newPicture
end

function image.expand(picture, fromTop, fromBottom, fromLeft, fromRight, background, foreground, alpha, symbol)
	local newPicture = image.create(picture[1] + fromRight + fromLeft, picture[2] + fromTop + fromBottom, background, foreground, alpha, symbol)

	for y = 1, picture[2] do
		for x = 1, picture[1] do
			image.set(newPicture, x + fromLeft, y + fromTop, image.get(picture, x, y))
		end
	end

	return newPicture
end

function image.blend(picture, blendColor, transparency)
	local newPicture = {picture[1], picture[2]}

	for i = 3, #picture, 4 do
		table.insert(newPicture, color.blend(picture[i], blendColor, transparency))
		table.insert(newPicture, color.blend(picture[i + 1], blendColor, transparency))
		table.insert(newPicture, picture[i + 2])
		table.insert(newPicture, picture[i + 3])
	end

	return newPicture
end

function image.rotate(picture, angle)
	local function copyPixel(newPic, oldPic, index)
		table.insert(newPic, oldPic[index])
		table.insert(newPic, oldPic[index + 1])
		table.insert(newPic, oldPic[index + 2])
		table.insert(newPic, oldPic[index + 3])
	end

	if angle == 90 then
		local newPicture = {picture[2], picture[1]}
		
		for i = 1, picture[2] do
			for j = picture[1], 1, -1 do
				copyPixel(newPicture, picture, image.getIndex(i, j, picture[2]))
			end
		end

		return newPicture
	elseif angle == 180 then
		local newPicture = {picture[1], picture[2]}
		
		for j = picture[1], 1, -1 do
			for i = picture[2], 1, -1 do
				copyPixel(newPicture, picture, image.getIndex(i, j, picture[2]))
			end
		end

		return newPicture
	elseif angle == 270 then
		local newPicture = {picture[2], picture[1]}
		
		for i = picture[2], 1, -1 do
			for j = 1, picture[1] do
				copyPixel(newPicture, picture, image.getIndex(i, j, picture[2]))
			end
		end

		return newPicture
	else
		error("Can't rotate image: angle must be 90, 180 or 270 degrees.")
	end
end

function image.hueSaturationBrightness(picture, hue, saturation, brightness)
	local function calculate(c)
		local h, s, b = color.integerToHSB(c)
		
		b = b + brightness; if b < 0 then b = 0 elseif b > 1 then b = 1 end
		s = s + saturation; if s < 0 then s = 0 elseif s > 1 then s = 1 end
		h = h + hue; if h < 0 then h = 0 elseif h > 360 then h = 360 end

		return color.HSBToInteger(h, s, b)
	end

	for i = 3, #picture, 4 do
		picture[i] = calculate(picture[i])
		picture[i + 1] = calculate(picture[i + 1])
	end

	return picture
end

function image.hue(picture, hue)
	return image.hueSaturationBrightness(picture, hue, 0, 0)
end

function image.saturation(picture, saturation)
	return image.hueSaturationBrightness(picture, 0, saturation, 0)
end

function image.brightness(picture, brightness)
	return image.hueSaturationBrightness(picture, 0, 0, brightness)
end

function image.blackAndWhite(picture)
	return image.hueSaturationBrightness(picture, 0, -1, 0)
end

function image.colorBalance(picture, r, g, b)
	local function calculate(c)
		local rr, gg, bb = color.integerToRGB(c)

		rr = rr + r
		gg = gg + g
		bb = bb + b
		
		if rr < 0 then rr = 0 elseif rr > 255 then rr = 255 end
		if gg < 0 then gg = 0 elseif gg > 255 then gg = 255 end
		if bb < 0 then bb = 0 elseif bb > 255 then bb = 255 end

		return color.RGBToInteger(rr, gg, bb)
	end

	for i = 3, #picture, 4 do
		picture[i] = calculate(picture[i])
		picture[i + 1] = calculate(picture[i + 1])
	end

	return picture
end

function image.invert(picture)
	for i = 3, #picture, 4 do
		picture[i] = 0xffffff - picture[i]
		picture[i + 1] = 0xffffff - picture[i + 1]
	end

	return picture 
end

function image.getGaussianBlurKernel(radius, weight)
	local size, index, sum, weightSquared2, value =
		radius * 2 + 1,
		2,
		0,
		2 * weight * weight

	local kernel, constant =
		{size},
		1 / (math.pi * weightSquared2)
	
	-- Filling convolution matrix
	for y = -radius, radius do
		for x = -radius, radius do
			value = constant * math.exp(-((y * y) + (x * x)) / weightSquared2);
			kernel[index] = value
			sum = sum + value;

			index = index + 1
		end
	end

	index = 2

	for y = 1, size do
		for x = 1, size do
			kernel[index] = kernel[index] * 1 / sum;

			index = index + 1
		end
	end

    return kernel;
end

function image.convolve(picture, kernel)
	-- Processing
	local
		pictureWidth,
		pictureHeight,
		kernelSize,
		pictureIndex,
		kernelIndex,
		kernelValue,
		rAcc,
		gAcc,
		bAcc,
		r,
		g,
		b,
		x,
		y = picture[1], picture[2], kernel[1], 3

	local newPicture, kernelRadius =
		{ pictureWidth, pictureHeight },
		math.floor(kernelSize / 2)

	for pictureY = 1, pictureHeight do
		for pictureX = 1, pictureWidth do
			rAcc, gAcc, bAcc, kernelIndex = 0, 0, 0, 2

			-- Summing
			for kernelY = -kernelRadius, kernelRadius do
				y = pictureY + kernelY

				if y >= 1 and y <= pictureHeight then
					for kernelX = -kernelRadius, kernelRadius do
						x = pictureX + kernelX

						if x >= 1 and x <= pictureWidth then
							kernelValue = kernel[kernelIndex]
							r, g, b = color.integerToRGB(picture[4 * (pictureWidth * (y - 1) + x) - 1])

							rAcc, gAcc, bAcc = rAcc + r * kernelValue, gAcc + g * kernelValue, bAcc + b * kernelValue
						end

						kernelIndex = kernelIndex + 1
					end
				else
					kernelIndex = kernelIndex + kernelSize
				end
			end

			-- Setting pixel values on new picture
			if rAcc > 255 then
				rAcc =  255
			elseif rAcc < 0 then
				rAcc = 0
			else
				rAcc = rAcc - rAcc % 1
			end

			if gAcc > 255 then
				gAcc =  255
			elseif gAcc < 0 then
				gAcc = 0
			else
				gAcc = gAcc - gAcc % 1
			end

			if bAcc > 255 then
				bAcc =  255
			elseif bAcc < 0 then
				bAcc = 0
			else
				bAcc = bAcc - bAcc % 1
			end

			newPicture[pictureIndex] = color.RGBToInteger(rAcc, gAcc, bAcc)
			pictureIndex = pictureIndex + 1

			newPicture[pictureIndex] = 0x0
			pictureIndex = pictureIndex + 1

			newPicture[pictureIndex] = picture[pictureIndex]
			pictureIndex = pictureIndex + 1

			newPicture[pictureIndex] = " "
			pictureIndex = pictureIndex + 1
		end
	end

	return newPicture
end

--------------------------------------------------------------------------------

return image
