
----------------------------------------- Libraries -----------------------------------------

require("advancedLua")
local computer = require("computer")
local keyboard = require("keyboard")
local fs = require("filesystem")
local unicode = require("unicode")
local event = require("event")
local color = require("color")
local image = require("image")
local buffer = require("doubleBuffering")

----------------------------------------- Constants -----------------------------------------

local GUI = {}

GUI.alignment = {
	horizontal = enum(
		"left",
		"center",
		"right"
	),
	vertical = enum(
		"top",
		"center",
		"bottom"
	)
}

GUI.directions = enum(
	"horizontal",
	"vertical"
)

GUI.sizePolicies = enum(
	"percentage",
	"absolute"
)

GUI.dropDownMenuElementTypes = enum(
	"default",
	"separator"
)

GUI.filesystemModes = enum(
	"file",
	"directory",
	"both",
	"open",
	"save"
)

GUI.colors = {
	disabled = {
		background = 0x888888,
		text = 0xAAAAAA
	},
	contextMenu = {
		separator = 0x888888,
		default = {
			background = 0xFFFFFF,
			text = 0x2D2D2D
		},
		disabled = 0x888888,
		pressed = {
			background = 0x3366CC,
			text = 0xFFFFFF
		},
		transparency = {
			background = 0.24,
			shadow = 0.4
		}
	},
	windows = {
		title = {
			background = 0xE1E1E1,
			text = 0x2D2D2D
		},
		backgroundPanel = 0xF0F0F0,
		tabBar = {
			default = {
				background = 0x2D2D2D,
				text = 0xF0F0F0
			},
			selected = {
				background = 0xF0F0F0,
				text = 0x2D2D2D
			}
		}
	}
}

----------------------------------------- Interface objects -----------------------------------------

function GUI.callMethod(method, ...)
	if method then method(...) end
end

local function objectIsClicked(object, x, y)
	return
		x >= object.x and
		y >= object.y and
		x <= object.x + object.width - 1 and
		y <= object.y + object.height - 1 and
		not object.disabled and
		not object.hidden
end

local function objectDraw(object)
	return object
end

function GUI.object(x, y, width, height)
	return {
		x = x,
		y = y,
		width = width,
		height = height,
		isClicked = objectIsClicked,
		draw = objectDraw
	}
end

----------------------------------------- Object alignment -----------------------------------------

function GUI.setAlignment(object, horizontalAlignment, verticalAlignment)
	object.alignment = {
		horizontal = horizontalAlignment,
		vertical = verticalAlignment
	}

	return object
end

function GUI.getAlignmentCoordinates(object, subObject)
	local x, y
	if object.alignment.horizontal == GUI.alignment.horizontal.left then
		x = object.x
	elseif object.alignment.horizontal == GUI.alignment.horizontal.center then
		x = math.floor(object.x + object.width / 2 - subObject.width / 2)
	elseif object.alignment.horizontal == GUI.alignment.horizontal.right then
		x = object.x + object.width - subObject.width
	else
		error("Unknown horizontal alignment: " .. tostring(object.alignment.horizontal))
	end

	if object.alignment.vertical == GUI.alignment.vertical.top then
		y = object.y
	elseif object.alignment.vertical == GUI.alignment.vertical.center then
		y = math.floor(object.y + object.height / 2 - subObject.height / 2)
	elseif object.alignment.vertical == GUI.alignment.vertical.bottom then
		y = object.y + object.height - subObject.height
	else
		error("Unknown vertical alignment: " .. tostring(object.alignment.vertical))
	end

	return x, y
end

function GUI.getMarginCoordinates(object)
	local x, y = object.x, object.y

	if object.alignment.horizontal == GUI.alignment.horizontal.left then
		x = x + object.margin.horizontal
	elseif object.alignment.horizontal == GUI.alignment.horizontal.right then
		x = x - object.margin.horizontal
	end

	if object.alignment.vertical == GUI.alignment.vertical.top then
		y = y + object.margin.vertical
	elseif object.alignment.vertical == GUI.alignment.vertical.bottom then
		y = y - object.margin.vertical
	end

	return x, y
end

----------------------------------------- Containers -----------------------------------------

local function checkObjectParentExists(object)
	if not object.parent then error("Object doesn't have a parent container") end
end

local function containerObjectIndexOf(object)
	checkObjectParentExists(object)

	for objectIndex = 1, #object.parent.children do
		if object.parent.children[objectIndex] == object then
			return objectIndex
		end
	end
end

local function containerObjectMoveForward(object)
	local objectIndex = object:indexOf()
	if objectIndex < #object.parent.children then
		object.parent.children[index], object.parent.children[index + 1] = swap(object.parent.children[index], object.parent.children[index + 1])
	end
	
	return object
end

local function containerObjectMoveBackward(object)
	local objectIndex = object:indexOf()
	if objectIndex > 1 then
		object.parent.children[objectIndex], object.parent.children[objectIndex - 1] = swap(object.parent.children[objectIndex], object.parent.children[objectIndex - 1])
	end
	
	return object
end

local function containerObjectMoveToFront(object)
	local objectIndex = object:indexOf()
	table.insert(object.parent.children, object)
	table.remove(object.parent.children, objectIndex)
	
	return object
end

local function containerObjectMoveToBack(object)
	local objectIndex = object:indexOf()
	table.insert(object.parent.children, 1, object)
	table.remove(object.parent.children, objectIndex + 1)
	
	return object
end

local function containerObjectGetFirstParent(object)
	if object.parent then
		local currentParent = object.parent
		while currentParent.parent do
			currentParent = currentParent.parent
		end

		return currentParent
	else
		error("Object doesn't have any parents")
	end
end

local function containerObjectSelfDelete(object)
	table.remove(object.parent.children, containerObjectIndexOf(object))
end

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

local function containerObjectAnimationStart(animation, duration)
	animation.position = 0
	animation.duration = duration
	animation.started = true
	animation.startUptime = computer.uptime()
	computer.pushSignal("GUIAnimationStart")
end

local function containerObjectAnimationStop(animation)
	animation.position = 0
	animation.started = false
end

local function containerObjectAnimationDelete(animation)
	animation.deleteLater = true
end

local function containerObjectAddAnimation(object, frameHandler, onFinish)
	local firstParent = object:getFirstParent()
	firstParent.animations = firstParent.animations or {}
	table.insert(firstParent.animations, {
		object = object,
		position = 0,
		start = containerObjectAnimationStart,
		stop = containerObjectAnimationStop,
		delete = containerObjectAnimationDelete,
		frameHandler = frameHandler,
		onFinish = onFinish,
	})

	return firstParent.animations[#firstParent.animations]
end

function GUI.addChildToContainer(container, object, atIndex)
	object.localPosition = {
		x = object.x,
		y = object.y
	}
	object.indexOf = containerObjectIndexOf
	object.moveToFront = containerObjectMoveToFront
	object.moveToBack = containerObjectMoveToBack
	object.moveForward = containerObjectMoveForward
	object.moveBackward = containerObjectMoveBackward
	object.getFirstParent = containerObjectGetFirstParent
	object.delete = containerObjectSelfDelete
	object.parent = container
	object.addAnimation = containerObjectAddAnimation

	if atIndex then
		table.insert(container.children, atIndex, object)
	else
		table.insert(container.children, object)
	end
	
	return object
end

local function deleteContainersContent(container, from, to)
	from = from or 1
	for objectIndex = from, to or #container.children do
		table.remove(container.children, from)
	end
end

local function getRectangleIntersection(R1X1, R1Y1, R1X2, R1Y2, R2X1, R2Y1, R2X2, R2Y2)
	if R2X1 <= R1X2 and R2Y1 <= R2Y2 and R2X2 >= R1X1 and R2Y2 >= R1Y1 then
		return
			math.max(R2X1, R1X1),
			math.max(R2Y1, R1Y1),
			math.min(R2X2, R1X2),
			math.min(R2Y2, R1Y2)
	else
		return
	end
end

function GUI.calculateChildAbsolutePosition(object)
	object.x, object.y = object.parent.x + object.localPosition.x - 1, object.parent.y + object.localPosition.y - 1
end

function GUI.drawContainerContent(container)
	local R1X1, R1Y1, R1X2, R1Y2 = buffer.getDrawLimit()
	local intersectionX1, intersectionY1, intersectionX2, intersectionY2 = getRectangleIntersection(R1X1, R1Y1, R1X2, R1Y2, container.x, container.y, container.x + container.width - 1, container.y + container.height - 1)

	if intersectionX1 then
		buffer.setDrawLimit(intersectionX1, intersectionY1, intersectionX2, intersectionY2)
		
		for objectIndex = 1, #container.children do
			if not container.children[objectIndex].hidden then
				GUI.calculateChildAbsolutePosition(container.children[objectIndex])
				container.children[objectIndex]:draw()
			end
		end

		buffer.setDrawLimit(R1X1, R1Y1, R1X2, R1Y2)
	end

	return container
end

local function containerHandler(isScreenEvent, mainContainer, currentContainer, eventData, x1, y1, x2, y2)
	local breakRecursion = false

	if not isScreenEvent or x1 and eventData[3] >= x1 and eventData[4] >= y1 and eventData[3] <= x2 and eventData[4] <= y2 then
		for i = #currentContainer.children, 1, -1 do
			if not currentContainer.children[i].hidden then
				if currentContainer.children[i].children then
					local intersectionX1, intersectionY1, intersectionX2, intersectionY2 = getRectangleIntersection(
						x1, y1, x2, y2,
						currentContainer.children[i].x,
						currentContainer.children[i].y,
						currentContainer.children[i].x + currentContainer.children[i].width - 1,
						currentContainer.children[i].y + currentContainer.children[i].height - 1
					)

					if intersectionX1 then
						if containerHandler(isScreenEvent, mainContainer, currentContainer.children[i], eventData, intersectionX1, intersectionY1, intersectionX2, intersectionY2) then
							breakRecursion = true
							break
						end
					end
				else
					if isScreenEvent then
						if currentContainer.children[i]:isClicked(eventData[3], eventData[4]) then
							GUI.callMethod(currentContainer.children[i].eventHandler, mainContainer, currentContainer.children[i], eventData)
							breakRecursion = true
							break
						end
					else
						GUI.callMethod(currentContainer.children[i].eventHandler, mainContainer, currentContainer.children[i], eventData)
					end
				end
			end
		end

		GUI.callMethod(currentContainer.eventHandler, mainContainer, currentContainer, eventData)
	end

	if breakRecursion then
		return true
	end
end

function GUI.isScreenEvent(eventType)
	return 
		eventType == "touch" or
		eventType == "drag" or
		eventType == "drop" or
		eventType == "scroll" or
		eventType == "double_touch"
end

local function containerStartEventHandling(container, eventHandlingDelay)
	container.eventHandlingDelay = eventHandlingDelay

	local eventData, animationIndex, needDraw
	while true do
		eventData = {event.pull(container.animations and 0 or container.eventHandlingDelay)}
		containerHandler(
			GUI.isScreenEvent(eventData[1]),
			container,
			container,
			eventData,
			container.x,
			container.y,
			container.x + container.width - 1,
			container.y + container.height - 1
		)

		if container.animations then
			animationIndex = 1
			while animationIndex <= #container.animations do
				if container.animations[animationIndex].started then
					needDraw = true
					container.animations[animationIndex].position = (computer.uptime() - container.animations[animationIndex].startUptime) / container.animations[animationIndex].duration
					
					if container.animations[animationIndex].position <= 1 then
						container.animations[animationIndex].frameHandler(container, container.animations[animationIndex].object, container.animations[animationIndex])
					else
						container.animations[animationIndex].position = 1
						container.animations[animationIndex].started = false
						container.animations[animationIndex].frameHandler(container, container.animations[animationIndex].object, container.animations[animationIndex])
						
						if container.animations[animationIndex].onFinish then
							needDraw = false
							container:draw()
							buffer.draw()
							container.animations[animationIndex].onFinish(container, container.animations[animationIndex].object, container.animations[animationIndex])
						end

						if container.animations[animationIndex].deleteLater then
							table.remove(container.animations, animationIndex)
							animationIndex = animationIndex - 1
						end
					end
				end

				animationIndex = animationIndex + 1
			end

			if needDraw then
				container:draw()
				buffer.draw()
			end

			if #container.animations == 0 then
				container.animations = nil
			end
		end

		if container.dataToReturn then
			local dataToReturn = container.dataToReturn
			container.dataToReturn = nil
			
			return table.unpack(dataToReturn)
		end
	end
end

local function containerReturnData(container, ...)
	container.dataToReturn = {...}
end

local function containerStopEventHandling(container)
	containerReturnData(container, nil)
end

function GUI.container(x, y, width, height)
	local container = GUI.object(x, y, width, height)

	container.children = {}
	container.draw = GUI.drawContainerContent
	container.deleteChildren = deleteContainersContent
	container.addChild = GUI.addChildToContainer
	container.returnData = containerReturnData
	container.startEventHandling = containerStartEventHandling
	container.stopEventHandling = containerStopEventHandling

	return container
end

function GUI.fullScreenContainer()
	return GUI.container(1, 1, buffer.width, buffer.height)
end

----------------------------------------- Buttons -----------------------------------------

local function buttonDraw(object)
	local xText, yText = GUI.getAlignmentCoordinates(object, {width = unicode.len(object.text), height = 1})

	local buttonColor, textColor = object.colors.default.background, object.colors.default.text
	if object.disabled then
		buttonColor, textColor = object.colors.disabled.background, object.colors.disabled.text
	else
		if object.pressed then
			buttonColor, textColor = object.colors.pressed.background, object.colors.pressed.text
		end
	end

	if buttonColor then
		if object.buttonType == 1 then
			buffer.square(object.x, object.y, object.width, object.height, buttonColor, textColor, " ")
		elseif object.buttonType == 2 then
			local x2, y2 = object.x + object.width - 1, object.y + object.height - 1
			
			if object.height > 1 then
				buffer.text(object.x + 1, object.y, buttonColor, string.rep("▄", object.width - 2))
				buffer.text(object.x, object.y, buttonColor, "⣠")
				buffer.text(x2, object.y, buttonColor, "⣄")
				
				buffer.square(object.x, object.y + 1, object.width, object.height - 2, buttonColor, textColor, " ")
				
				buffer.text(object.x + 1, y2, buttonColor, string.rep("▀", object.width - 2))
				buffer.text(object.x, y2, buttonColor, "⠙")
				buffer.text(x2, y2, buttonColor, "⠋")
			else
				buffer.square(object.x, object.y, object.width, object.height, buttonColor, textColor, " ")
				GUI.roundedCorners(object.x, object.y, object.width, object.height, buttonColor)
			end
		else
			buffer.frame(object.x, object.y, object.width, object.height, buttonColor)
		end
	end

	buffer.text(xText, yText, textColor, object.text)

	return object
end

local function buttonPress(object)
	object.pressed = true
	buttonDraw(object)
end

local function buttonRelease(object)
	object.pressed = nil
	buttonDraw(object)
end

local function buttonPressAndRelease(object, pressTime)
	buttonPress(object)
	buffer.draw()
	os.sleep(pressTime or 0.2)
	buttonRelease(object)
	buffer.draw()
end

local function buttonEventHandler(mainContainer, object, eventData)
	if eventData[1] == "touch" then
		if object.switchMode then
			object.pressed = not object.pressed
			mainContainer:draw()
			buffer.draw()
			GUI.callMethod(object.onTouch, mainContainer, object, eventData)
		else
			object.pressed = true
			mainContainer:draw()
			buffer.draw()
			os.sleep(0.2)
			object.pressed = false
			mainContainer:draw()
			buffer.draw()
			GUI.callMethod(object.onTouch, mainContainer, object, eventData)
		end
	end
end

local function buttonCreate(buttonType, x, y, width, height, buttonColor, textColor, buttonPressedColor, textPressedColor, text, disabledState)
	local object = GUI.object(x, y, width, height)

	object.colors = {
		default = {
			background = buttonColor,
			text = textColor
		},
		pressed = {
			background = buttonPressedColor,
			text = textPressedColor
		},
		disabled = {
			background = GUI.colors.disabled.background,
			text = GUI.colors.disabled.text,
		}
	}

	object.eventHandler = buttonEventHandler
	object.buttonType = buttonType
	object.disabled = disabledState
	object.text = text
	object.press = buttonPress
	object.release = buttonRelease
	object.pressAndRelease = buttonPressAndRelease
	object.draw = buttonDraw
	object.setAlignment = GUI.setAlignment
	object:setAlignment(GUI.alignment.horizontal.center, GUI.alignment.vertical.center)


	return object
end

function GUI.button(...)
	return buttonCreate(1, ...)
end

function GUI.adaptiveButton(x, y, xOffset, yOffset, buttonColor, textColor, buttonPressedColor, textPressedColor, text, ...) 
	return buttonCreate(1, x, y, unicode.len(text) + xOffset * 2, yOffset * 2 + 1, buttonColor, textColor, buttonPressedColor, textPressedColor, text, ...)
end

function GUI.roundedButton(...)
	return buttonCreate(2, ...)
end

function GUI.adaptiveRoundedButton(x, y, xOffset, yOffset, buttonColor, textColor, buttonPressedColor, textPressedColor, text, ...)
	return buttonCreate(2, x, y, unicode.len(text) + xOffset * 2, yOffset * 2 + 1, buttonColor, textColor, buttonPressedColor, textPressedColor, text, ...)
end

function GUI.framedButton(...)
	return buttonCreate(3, ...)
end

function GUI.adaptiveFramedButton(x, y, xOffset, yOffset, buttonColor, textColor, buttonPressedColor, textPressedColor, text, ...)
	return buttonCreate(3, x, y, unicode.len(text) + xOffset * 2, yOffset * 2 + 1, buttonColor, textColor, buttonPressedColor, textPressedColor, text, ...)
end

----------------------------------------- TabBar -----------------------------------------

local function tabBarTabEventHandler(mainContainer, object, eventData)
	if eventData[1] == "touch" then
		object.parent.selectedItem = object:indexOf() - 1
		mainContainer:draw()
		buffer.draw()
		GUI.callMethod(object.onTouch, mainContainer, object, eventData)
	end
end

local function tabBarDraw(tabBar)
	tabBar.backgroundPanel.width, tabBar.backgroundPanel.height, tabBar.backgroundPanel.colors.background = tabBar.width, tabBar.height, tabBar.colors.default.background
	
	local totalWidth = 0
	for i = 2, #tabBar.children do
		totalWidth = totalWidth + tabBar.children[i].width + tabBar.spaceBetweenTabs
	end
	totalWidth = totalWidth - tabBar.spaceBetweenTabs

	local x = math.floor(tabBar.width / 2 - totalWidth / 2)
	for i = 2, #tabBar.children do
		tabBar.children[i].localPosition.x = x
		x = x + tabBar.children[i].width + tabBar.spaceBetweenTabs
		tabBar.children[i].pressed = (i - 1) == tabBar.selectedItem
	end

	GUI.drawContainerContent(tabBar)

	return tabBar
end

local function tabBarAddItem(tabBar, text)
	local item = tabBar:addChild(GUI.button(1, 1, unicode.len(text) + tabBar.horizontalTabOffset * 2, tabBar.height, tabBar.colors.default.background, tabBar.colors.default.text, tabBar.colors.selected.background, tabBar.colors.selected.text, text))
	
	item.switchMode = true
	item.eventHandler = tabBarTabEventHandler

	return item
end

function GUI.tabBar(x, y, width, height, horizontalTabOffset, spaceBetweenTabs, backgroundColor, textColor, backgroundSelectedColor, textSelectedColor, ...)
	local tabBar = GUI.container(x, y, width, height)

	tabBar.backgroundPanel = tabBar:addChild(GUI.panel(1, 1, 1, 1, backgroundColor))
	tabBar.horizontalTabOffset = horizontalTabOffset
	tabBar.spaceBetweenTabs = spaceBetweenTabs
	tabBar.colors = {
		default = {
			background = backgroundColor,
			text = textColor
		},
		selected = {
			background = backgroundSelectedColor,
			text = textSelectedColor
		}
	}
	tabBar.selectedItem = 1
	tabBar.draw = tabBarDraw
	tabBar.addItem = tabBarAddItem

	local items = {...}
	for i = 1, #items do
		tabBar:addItem(items[i])
	end

	return tabBar
end

----------------------------------------- Panel -----------------------------------------

local function drawPanel(object)
	buffer.square(object.x, object.y, object.width, object.height, object.colors.background, 0x000000, " ", object.colors.transparency)
	return object
end

function GUI.panel(x, y, width, height, color, transparency)
	local object = GUI.object(x, y, width, height)
	
	object.colors = {
		background = color,
		transparency = transparency
	}
	object.draw = drawPanel
	
	return object
end

----------------------------------------- Label -----------------------------------------

local function drawLabel(object)
	local xText, yText = GUI.getAlignmentCoordinates(object, {width = unicode.len(object.text), height = 1})
	buffer.text(xText, yText, object.colors.text, object.text)
	return object
end

function GUI.label(x, y, width, height, textColor, text)
	local object = GUI.object(x, y, width, height)
	object.setAlignment = GUI.setAlignment
	object:setAlignment(GUI.alignment.horizontal.left, GUI.alignment.vertical.top)
	object.colors = {text = textColor}
	object.text = text
	object.draw = drawLabel
	return object
end

----------------------------------------- Image -----------------------------------------

local function drawImage(object)
	buffer.image(object.x, object.y, object.image)
	return object
end

function GUI.image(x, y, image)
	local object = GUI.object(x, y, image[1], image[2])
	object.image = image
	object.draw = drawImage
	return object
end

----------------------------------------- Action buttons -----------------------------------------

function GUI.actionButtons(x, y, fatSymbol)
	local symbol = fatSymbol and "⬤" or "●"
	
	local container = GUI.container(x, y, 5, 1)
	container.close = container:addChild(GUI.button(1, 1, 1, 1, nil, 0xFF4940, nil, 0x992400, symbol))
	container.minimize = container:addChild(GUI.button(3, 1, 1, 1, nil, 0xFFB640, nil, 0x996D00, symbol))
	container.maximize = container:addChild(GUI.button(5, 1, 1, 1, nil, 0x00B640, nil, 0x006D40, symbol))

	return container
end

----------------------------------------- Menu -----------------------------------------

local function menuDraw(menu)
	buffer.square(menu.x, menu.y, menu.width, 1, menu.colors.default.background, menu.colors.default.text, " ", menu.colors.transparency)
	menu:reimplementedDraw()
end

local function menuItemEventHandler(mainContainer, object, eventData)
	if eventData[1] == "touch" then
		if object.onTouch then
			object.pressed = true
			mainContainer:draw()
			buffer.draw()

			object.onTouch(eventData)
			
			object.pressed = false
			mainContainer:draw()
			buffer.draw()
		end
	end
end

local function menuAddItem(menu, text, textColor)
	local x = 2; for i = 1, #menu.children do x = x + unicode.len(menu.children[i].text) + 2; end
	local item = menu:addChild(GUI.adaptiveButton(x, 1, 1, 0, nil, textColor or menu.colors.default.text, menu.colors.pressed.background, menu.colors.pressed.text, text))
	item.eventHandler = menuItemEventHandler

	return item
end

function GUI.menu(x, y, width, backgroundColor, textColor, backgroundPressedColor, textPressedColor, backgroundTransparency)
	local menu = GUI.container(x, y, width, 1)
	
	menu.colors = {
		default = {
			background = backgroundColor,
			text = textColor,
		},
		pressed = {
			background = backgroundPressedColor,
			text = textPressedColor,
		},
		transparency = backgroundTransparency
	}
	menu.addItem = menuAddItem
	menu.reimplementedDraw = menu.draw
	menu.draw = menuDraw

	return menu
end

----------------------------------------- ProgressBar Object -----------------------------------------

local function drawProgressBar(object)
	local activeWidth = math.floor(object.value * object.width / 100)
	if object.thin then
		buffer.text(object.x, object.y, object.colors.passive, string.rep("━", object.width))
		buffer.text(object.x, object.y, object.colors.active, string.rep("━", activeWidth))
	else
		buffer.square(object.x, object.y, object.width, object.height, object.colors.passive, 0x0, " ")
		buffer.square(object.x, object.y, activeWidth, object.height, object.colors.active, 0x0, " ")
	end

	if object.showValue then
		local stringValue = tostring((object.valuePrefix or "") .. object.value .. (object.valuePostfix or ""))
		buffer.text(math.floor(object.x + object.width / 2 - unicode.len(stringValue) / 2), object.y + 1, object.colors.value, stringValue)
	end

	return object
end

function GUI.progressBar(x, y, width, activeColor, passiveColor, valueColor, value, thin, showValue, valuePrefix, valuePostfix)
	local object = GUI.object(x, y, width, 1)
	
	object.value = value
	object.colors = {active = activeColor, passive = passiveColor, value = valueColor}
	object.thin = thin
	object.draw = drawProgressBar
	object.showValue = showValue
	object.valuePrefix = valuePrefix
	object.valuePostfix = valuePostfix
	
	return object
end

----------------------------------------- Other GUI elements -----------------------------------------

function GUI.windowShadow(x, y, width, height, transparency, thin)
	transparency = transparency
	if thin then
		buffer.square(x + width, y + 1, 1, height - 1, 0x000000, 0x000000, " ", transparency)
		buffer.text(x + 1, y + height, 0x000000, string.rep("▀", width), transparency)
		buffer.text(x + width, y, 0x000000, "▄", transparency)
	else
		buffer.square(x + width, y + 1, 2, height, 0x000000, 0x000000, " ", transparency)
		buffer.square(x + 2, y + height, width - 2, 1, 0x000000, 0x000000, " ", transparency)
	end
end

function GUI.roundedCorners(x, y, width, height, color, transparency)
	buffer.text(x - 1, y, color, "⠰", transparency)
	buffer.text(x + width, y, color, "⠆", transparency)
end

------------------------------------------------- Error window -------------------------------------------------------------------

function GUI.error(...)
	local args = {...}
	for i = 1, #args do
		if type(args[i]) == "table" then
			args[i] = table.toString(args[i], true)
		else
			args[i] = tostring(args[i])
		end
	end
	if #args == 0 then args[1] = "nil" end

	local sign = image.fromString([[06030000FF 0000FF 00F7FF▟00F7FF▙0000FF 0000FF 0000FF 00F7FF▟F7FF00 F7FF00 00F7FF▙0000FF 00F7FF▟F7FF00CF7FF00yF7FF00kF7FF00a00F7FF▙]])
	local offset = 2
	local lines = #args > 1 and "\"" .. table.concat(args, "\", \"") .. "\"" or args[1]
	local width = math.floor(buffer.width * 0.5)
	local textWidth = width - image.getWidth(sign) - 2

	lines = string.wrap(lines, textWidth)
	local height = image.getHeight(sign)
	if #lines + 2 > height then
		height = #lines + 2
	end

	local mainContainer = GUI.container(1, math.floor(buffer.height / 2 - height / 2), buffer.width, height + offset * 2)
	local oldPixels = buffer.copy(mainContainer.x, mainContainer.y, mainContainer.width, mainContainer.height)

	local x, y = math.floor(buffer.width / 2 - width / 2), offset + 1
	mainContainer:addChild(GUI.panel(1, 1, mainContainer.width, mainContainer.height, 0x1D1D1D))
	mainContainer:addChild(GUI.image(x, y, sign))
	mainContainer:addChild(GUI.textBox(x + image.getWidth(sign) + 2, y, textWidth, #lines, 0x1D1D1D, 0xE1E1E1, lines, 1, 0, 0)).eventHandler = nil
	local buttonWidth = 10
	local button = mainContainer:addChild(GUI.roundedButton(x + image.getWidth(sign) + textWidth - buttonWidth + 2, mainContainer.height - offset, buttonWidth, 1, 0x3366CC, 0xE1E1E1, 0xE1E1E1, 0x3366CC, "OK"))
	
	button.onTouch = function()
		mainContainer:stopEventHandling()
		buffer.paste(mainContainer.x, mainContainer.y, oldPixels)
		buffer.draw()
	end

	mainContainer.eventHandler = function(mainContainer, object, eventData)
		if eventData[1] == "key_down" and eventData[4] == 28 then
			button:pressAndRelease()
			button.onTouch()
		end
	end

	mainContainer:draw()
	buffer.draw(true)
	mainContainer:startEventHandling()
end

----------------------------------------- CodeView object -----------------------------------------

local function codeViewDraw(codeView)
	local syntax = require("syntax")
	local toLine = codeView.fromLine + codeView.height - 1

	-- Line numbers bar and code area
	codeView.lineNumbersWidth = unicode.len(tostring(toLine)) + 2
	codeView.codeAreaPosition = codeView.x + codeView.lineNumbersWidth
	codeView.codeAreaWidth = codeView.width - codeView.lineNumbersWidth
	buffer.square(codeView.x, codeView.y, codeView.lineNumbersWidth, codeView.height, syntax.colorScheme.lineNumbersBackground, syntax.colorScheme.lineNumbersText, " ")	
	buffer.square(codeView.codeAreaPosition, codeView.y, codeView.codeAreaWidth, codeView.height, syntax.colorScheme.background, syntax.colorScheme.text, " ")

	-- Line numbers texts
	local y = codeView.y
	for line = codeView.fromLine, toLine do
		if codeView.lines[line] then
			local text = tostring(line)
			if codeView.highlights[line] then
				buffer.square(codeView.x, y, codeView.lineNumbersWidth, 1, codeView.highlights[line], syntax.colorScheme.text, " ", 0.3)
				buffer.square(codeView.codeAreaPosition, y, codeView.codeAreaWidth, 1, codeView.highlights[line], syntax.colorScheme.text, " ")
			end
			buffer.text(codeView.codeAreaPosition - unicode.len(text) - 1, y, syntax.colorScheme.lineNumbersText, text)
			y = y + 1
		else
			break
		end	
	end

	local oldDrawLimitX1, oldDrawLimitY1, oldDrawLimitX2, oldDrawLimitY2 = buffer.getDrawLimit()
	buffer.setDrawLimit(codeView.codeAreaPosition, codeView.y, codeView.codeAreaPosition + codeView.codeAreaWidth - 1, codeView.y + codeView.height - 1)

	local function drawUpperSelection(y, selectionIndex)
		buffer.square(
			codeView.codeAreaPosition + codeView.selections[selectionIndex].from.symbol - codeView.fromSymbol + 1,
			y + codeView.selections[selectionIndex].from.line - codeView.fromLine,
			codeView.codeAreaWidth - codeView.selections[selectionIndex].from.symbol + codeView.fromSymbol - 1,
			1,
			codeView.selections[selectionIndex].color or syntax.colorScheme.selection, syntax.colorScheme.text, " "
		)
	end

	local function drawLowerSelection(y, selectionIndex)
		buffer.square(
			codeView.codeAreaPosition,
			y + codeView.selections[selectionIndex].from.line - codeView.fromLine,
			codeView.selections[selectionIndex].to.symbol - codeView.fromSymbol + 2,
			1,
			codeView.selections[selectionIndex].color or syntax.colorScheme.selection, syntax.colorScheme.text, " "
		)
	end

	if #codeView.selections > 0 then
		for selectionIndex = 1, #codeView.selections do
			y = codeView.y
			local dy = codeView.selections[selectionIndex].to.line - codeView.selections[selectionIndex].from.line
			if dy == 0 then
				buffer.square(
					codeView.codeAreaPosition + codeView.selections[selectionIndex].from.symbol - codeView.fromSymbol + 1,
					y + codeView.selections[selectionIndex].from.line - codeView.fromLine,
					codeView.selections[selectionIndex].to.symbol - codeView.selections[selectionIndex].from.symbol + 1,
					1,
					codeView.selections[selectionIndex].color or syntax.colorScheme.selection, syntax.colorScheme.text, " "
				)
			elseif dy == 1 then
				drawUpperSelection(y, selectionIndex); y = y + 1
				drawLowerSelection(y, selectionIndex)
			else
				drawUpperSelection(y, selectionIndex); y = y + 1
				for i = 1, dy - 1 do
					buffer.square(codeView.codeAreaPosition, y + codeView.selections[selectionIndex].from.line - codeView.fromLine, codeView.codeAreaWidth, 1, codeView.selections[selectionIndex].color or syntax.colorScheme.selection, syntax.colorScheme.text, " "); y = y + 1
				end

				drawLowerSelection(y, selectionIndex)
			end
		end
	end

	-- Code strings
	y = codeView.y
	buffer.setDrawLimit(codeView.codeAreaPosition + 1, y, codeView.codeAreaPosition + codeView.codeAreaWidth - 2, y + codeView.height - 1)
	for i = codeView.fromLine, toLine do
		if codeView.lines[i] then
			if codeView.highlightLuaSyntax then
				syntax.highlightString(codeView.codeAreaPosition - codeView.fromSymbol + 2, y, codeView.lines[i], codeView.indentationWidth)
			else
				buffer.text(codeView.codeAreaPosition - codeView.fromSymbol + 2, y, syntax.colorScheme.text, codeView.lines[i])
			end
			y = y + 1
		else
			break
		end
	end
	buffer.setDrawLimit(oldDrawLimitX1, oldDrawLimitY1, oldDrawLimitX2, oldDrawLimitY2)

	if #codeView.lines > codeView.height then
		codeView.scrollBars.vertical.hidden = false
		codeView.scrollBars.vertical.colors.background, codeView.scrollBars.vertical.colors.foreground = syntax.colorScheme.scrollBarBackground, syntax.colorScheme.scrollBarForeground
		codeView.scrollBars.vertical.minimumValue, codeView.scrollBars.vertical.maximumValue, codeView.scrollBars.vertical.value, codeView.scrollBars.vertical.shownValueCount = 1, #codeView.lines, codeView.fromLine, codeView.height
		codeView.scrollBars.vertical.localPosition.x = codeView.width
		codeView.scrollBars.vertical.localPosition.y = 1
		codeView.scrollBars.vertical.height = codeView.height - 1
	else
		codeView.scrollBars.vertical.hidden = true
	end

	if codeView.maximumLineLength > codeView.codeAreaWidth - 2 then
		codeView.scrollBars.horizontal.hidden = false
		codeView.scrollBars.horizontal.colors.background, codeView.scrollBars.horizontal.colors.foreground = syntax.colorScheme.scrollBarBackground, syntax.colorScheme.scrollBarForeground
		codeView.scrollBars.horizontal.minimumValue, codeView.scrollBars.horizontal.maximumValue, codeView.scrollBars.horizontal.value, codeView.scrollBars.horizontal.shownValueCount = 1, codeView.maximumLineLength, codeView.fromSymbol, codeView.codeAreaWidth - 2
		codeView.scrollBars.horizontal.localPosition.x = codeView.lineNumbersWidth + 1
		codeView.scrollBars.horizontal.localPosition.y = codeView.height
		codeView.scrollBars.horizontal.width = codeView.codeAreaWidth - 1
	else
		codeView.scrollBars.horizontal.hidden = true
	end

	codeView:reimplementedDraw()
end

function GUI.codeView(x, y, width, height, lines, fromSymbol, fromLine, maximumLineLength, selections, highlights, highlightLuaSyntax, indentationWidth)
	local codeView = GUI.container(x, y, width, height)
	
	codeView.lines = lines
	codeView.fromSymbol = fromSymbol
	codeView.fromLine = fromLine
	codeView.maximumLineLength = maximumLineLength
	codeView.selections = selections or {}
	codeView.highlights = highlights or {}
	codeView.highlightLuaSyntax = highlightLuaSyntax
	codeView.indentationWidth = indentationWidth

	codeView.scrollBars = {
		vertical = codeView:addChild(GUI.scrollBar(1, 1, 1, 1, 0x0, 0x0, 1, 1, 1, 1, 1, true)),
		horizontal = codeView:addChild(GUI.scrollBar(1, 1, 1, 1, 0x0, 0x0, 1, 1, 1, 1, 1, true))
	}

	codeView.reimplementedDraw = codeView.draw
	codeView.draw = codeViewDraw

	return codeView
end 

----------------------------------------- Color Selector object -----------------------------------------

local function colorSelectorDraw(colorSelector)
	local overlayColor = colorSelector.color < 0x7FFFFF and 0xFFFFFF or 0x000000
	buffer.square(colorSelector.x, colorSelector.y, colorSelector.width, colorSelector.height, colorSelector.color, overlayColor, " ")
	if colorSelector.pressed then
		buffer.square(colorSelector.x, colorSelector.y, colorSelector.width, colorSelector.height, overlayColor, overlayColor, " ", 0.8)
	end
	if colorSelector.height > 1 then
		buffer.text(colorSelector.x, colorSelector.y + colorSelector.height - 1, overlayColor, string.rep("▄", colorSelector.width), 0.8)
	end
	buffer.text(colorSelector.x + 1, colorSelector.y + math.floor(colorSelector.height / 2), overlayColor, string.limit(colorSelector.text, colorSelector.width - 2))
	return colorSelector
end

local function colorSelectorEventHandler(mainContainer, object, eventData)
	if eventData[1] == "touch" then
		object.pressed = true
		mainContainer:draw()
		buffer.draw()
		
		object.color = require("palette").show(math.floor(mainContainer.width / 2 - 35), math.floor(mainContainer.height / 2 - 12), object.color) or object.color
		
		object.pressed = false
		mainContainer:draw()
		buffer.draw()
		GUI.callMethod(object.onTouch, eventData)
	end
end

function GUI.colorSelector(x, y, width, height, color, text)
	local colorSelector = GUI.object(x, y, width, height)
	
	colorSelector.eventHandler = colorSelectorEventHandler
	colorSelector.color = color
	colorSelector.text = text
	colorSelector.draw = colorSelectorDraw
	
	return colorSelector
end 

----------------------------------------- Chart object -----------------------------------------

local function getAxisValue(number, postfix, roundValues)
	if roundValues then
		return math.floor(number) .. postfix
	else
		local integer, fractional = math.modf(number)
		local firstPart, secondPart = "", ""
		if math.abs(integer) >= 1000 then
			return math.shortenNumber(integer, 2) .. postfix
		else
			if math.abs(fractional) > 0 then
				return string.format("%.2f", number) .. postfix
			else
				return number .. postfix
			end
		end
	end
end

local function drawChart(object)
	-- Sorting by x value
	local valuesCopy = {}
	for i = 1, #object.values do valuesCopy[i] = object.values[i] end
	table.sort(valuesCopy, function(a, b) return a[1] < b[1] end)
	
	if #valuesCopy == 0 then valuesCopy = {{0, 0}} end

	-- Max, min, deltas
	local xMin, xMax, yMin, yMax = valuesCopy[1][1], valuesCopy[#valuesCopy][1], valuesCopy[1][2], valuesCopy[1][2]
	for i = 1, #valuesCopy do yMin, yMax = math.min(yMin, valuesCopy[i][2]), math.max(yMax, valuesCopy[i][2]) end
	local dx, dy = xMax - xMin, yMax - yMin

	-- y axis values and helpers
	local value, chartHeight, yAxisValueMaxWidth, yAxisValues = yMin, object.height - 1 - (object.showXAxisValues and 1 or 0), 0, {}
	for y = object.y + object.height - 3, object.y + 1, -chartHeight * object.yAxisValueInterval do
		local stringValue = getAxisValue(value, object.yAxisPostfix, object.roundValues)
		yAxisValueMaxWidth = math.max(yAxisValueMaxWidth, unicode.len(stringValue))
		table.insert(yAxisValues, {y = math.ceil(y), value = stringValue})
		value = value + dy * object.yAxisValueInterval
	end
	local stringValue = getAxisValue(yMax, object.yAxisPostfix, object.roundValues)
	table.insert(yAxisValues, {y = object.y, value = stringValue})
	yAxisValueMaxWidth = math.max(yAxisValueMaxWidth, unicode.len(stringValue))

	local chartWidth = object.width - (object.showYAxisValues and yAxisValueMaxWidth + 2 or 0) 
	local chartX = object.x + object.width - chartWidth
	for i = 1, #yAxisValues do
		if object.showYAxisValues then
			buffer.text(chartX - unicode.len(yAxisValues[i].value) - 2, yAxisValues[i].y, object.colors.axisValue, yAxisValues[i].value)
		end
		buffer.text(chartX, yAxisValues[i].y, object.colors.helpers, string.rep("─", chartWidth))
	end

	-- x axis values
	if object.showXAxisValues then
		value = xMin
		for x = chartX, chartX + chartWidth - 2, chartWidth * object.xAxisValueInterval do
			local stringValue = getAxisValue(value, object.xAxisPostfix, object.roundValues)
			buffer.text(math.floor(x - unicode.len(stringValue) / 2), object.y + object.height - 1, object.colors.axisValue, stringValue)
			value = value + dx * object.xAxisValueInterval
		end
		local value = getAxisValue(xMax, object.xAxisPostfix, object.roundValues)
		buffer.text(object.x + object.width - unicode.len(value), object.y + object.height - 1, object.colors.axisValue, value)
	end

	-- Axis lines
	for y = object.y, object.y + chartHeight - 1 do
		buffer.text(chartX - 1, y, object.colors.axis, "┨")
	end
	buffer.text(chartX - 1, object.y + chartHeight, object.colors.axis, "┗" .. string.rep("┯━", math.floor(chartWidth / 2)))

	local function fillVerticalPart(x1, y1, x2, y2)
		local dx, dy = x2 - x1, y2 - y1
		local absdx, absdy = math.abs(dx), math.abs(dy)
		if absdx >= absdy then
			local step, y = dy / absdx, y1
			for x = x1, x2, (x1 < x2 and 1 or -1) do
				local yFloor = math.floor(y)
				buffer.semiPixelSquare(math.floor(x), yFloor, 1, math.floor(object.y + chartHeight) * 2 - yFloor - 1, object.colors.chart)
				y = y + step
			end
		else
			local step, x = dx / absdy, x1
			for y = y1, y2, (y1 < y2 and 1 or -1) do
				local yFloor = math.floor(y)
				buffer.semiPixelSquare(math.floor(x), yFloor, 1, math.floor(object.y + chartHeight) * 2 - yFloor - 1, object.colors.chart)
				x = x + step
			end
		end
	end

	-- chart
	for i = 1, #valuesCopy - 1 do
		local x = math.floor(chartX + (valuesCopy[i][1] - xMin) / dx * (chartWidth - 1))
		local y = math.floor(object.y + chartHeight - 1 - (valuesCopy[i][2] - yMin) / dy * (chartHeight - 1)) * 2
		local xNext = math.floor(chartX + (valuesCopy[i + 1][1] - xMin) / dx * (chartWidth - 1))
		local yNext = math.floor(object.y + chartHeight - 1 - (valuesCopy[i + 1][2] - yMin) / dy * (chartHeight - 1)) * 2
		if object.fillChartArea then
			fillVerticalPart(x, y, xNext, yNext)
		else
			buffer.semiPixelLine(x, y, xNext, yNext, object.colors.chart)
		end
	end

	return object
end

function GUI.chart(x, y, width, height, axisColor, axisValueColor, axisHelpersColor, chartColor, xAxisValueInterval, yAxisValueInterval, xAxisPostfix, yAxisPostfix, fillChartArea, values)
	local object = GUI.object(x, y, width, height)

	object.colors = {axis = axisColor, chart = chartColor, axisValue = axisValueColor, helpers = axisHelpersColor}
	object.draw = drawChart
	object.values = values or {}
	object.xAxisPostfix = xAxisPostfix
	object.yAxisPostfix = yAxisPostfix
	object.xAxisValueInterval = xAxisValueInterval
	object.yAxisValueInterval = yAxisValueInterval
	object.fillChartArea = fillChartArea
	object.showYAxisValues = true
	object.showXAxisValues = true

	return object
end

----------------------------------------- Window object -----------------------------------------

local function windowDraw(window)
	GUI.windowShadow(window.x, window.y, window.width, window.height, nil, true)
	GUI.drawContainerContent(window)
	return window
end

local function windowCheck(container, x, y)
	for i = #container.children, 1, -1 do
		if container.children[i].children then
			if windowCheck(container.children[i], x, y) then
				return true
			end
		elseif container.children[i].eventHandler and container.children[i]:isClicked(x, y) then
			return true
		end
	end
end

local function windowEventHandler(mainContainer, object, eventData)
	if eventData[1] == "touch" then
		object.lastTouchPosition = object.lastTouchPosition or {}
		object.lastTouchPosition.x, object.lastTouchPosition.y = eventData[3], eventData[4]
		
		if object ~= object.parent.children[#object.parent.children] then
			object:moveToFront()
			mainContainer:draw()
			buffer.draw()
		end
	elseif eventData[1] == "drag" and object.lastTouchPosition and not windowCheck(object, eventData[3], eventData[4]) then
		local xOffset, yOffset = eventData[3] - object.lastTouchPosition.x, eventData[4] - object.lastTouchPosition.y
		object.lastTouchPosition.x, object.lastTouchPosition.y = eventData[3], eventData[4]

		if xOffset ~= 0 or yOffset ~= 0 then
			object.localPosition.x, object.localPosition.y = object.localPosition.x + xOffset, object.localPosition.y + yOffset
			mainContainer:draw()
			buffer.draw()
		end
	elseif eventData[1] == "drop" then
		object.lastTouchPosition = nil
	elseif eventData[1] == "key_down" then
		-- Ctrl or CMD
		if keyboard.isKeyDown(29) or keyboard.isKeyDown(219) then
			-- W
			if eventData[4] == 17 then
				if object == object.parent.children[#object.parent.children] and not eventData.windowHandled then
					eventData.windowHandled = true
					object:close()
					mainContainer:draw()
					buffer.draw()
				end
			end
		end
	end
end

function GUI.window(x, y, width, height)
	local window = GUI.container(x, y, width, height)
	
	window.eventHandler = windowEventHandler
	window.draw = windowDraw

	return window
end

function GUI.filledWindow(x, y, width, height, backgroundColor)
	local window = GUI.window(x, y, width, height)

	window.backgroundPanel = window:addChild(GUI.panel(1, 1, width, height, backgroundColor))
	window.actionButtons = window:addChild(GUI.actionButtons(2, 1, false))

	return window
end

function GUI.titledWindow(x, y, width, height, title, addTitlePanel)
	local window = GUI.filledWindow(x, y, width, height, GUI.colors.windows.backgroundPanel)

	if addTitlePanel then
		window.titlePanel = window:addChild(GUI.panel(1, 1, width, 1, GUI.colors.windows.title.background))
		window.backgroundPanel.localPosition.y, window.backgroundPanel.height = 2, window.height - 1
	end
	window.titleLabel = window:addChild(GUI.label(1, 1, width, height, GUI.colors.windows.title.text, title)):setAlignment(GUI.alignment.horizontal.center, GUI.alignment.vertical.top)
	window.actionButtons:moveToFront()

	return window
end

function GUI.tabbedWindow(x, y, width, height, ...)
	local window = GUI.filledWindow(x, y, width, height, GUI.colors.windows.backgroundPanel)

	window.tabBar = window:addChild(GUI.tabBar(1, 1, window.width, 3, 2, 0, GUI.colors.windows.tabBar.default.background, GUI.colors.windows.tabBar.default.text, GUI.colors.windows.tabBar.selected.background, GUI.colors.windows.tabBar.selected.text, ...))
	window.backgroundPanel.localPosition.y, window.backgroundPanel.height = 4, window.height - 3
	window.actionButtons:moveToFront()
	window.actionButtons.localPosition.y = 2

	return window
end

----------------------------------------- Dropdown Menu -----------------------------------------

local function dropDownMenuItemDraw(item)
	local yText = item.y + math.floor(item.height / 2)

	if item.type == GUI.dropDownMenuElementTypes.default then
		local textColor = item.color or item.parent.parent.colors.default.text

		if item.pressed then
			textColor = item.parent.parent.colors.pressed.text
			buffer.square(item.x, item.y, item.width, item.height, item.parent.parent.colors.pressed.background, textColor, " ")
		elseif item.disabled then
			textColor = item.parent.parent.colors.disabled.text
		end

		buffer.text(item.x + 1, yText, textColor, item.text)
		if item.shortcut then
			buffer.text(item.x + item.width - unicode.len(item.shortcut) - 1, yText, textColor, item.shortcut)
		end
	else
		buffer.text(item.x, yText, item.parent.parent.colors.separator, string.rep("─", item.width))
	end

	return item
end

local function dropDownMenuItemEventHandler(mainContainer, object, eventData)
	if eventData[1] == "touch" then
		if object.type == GUI.dropDownMenuElementTypes.default then
			object.pressed = true
			mainContainer:draw()
			buffer.draw()

			if object.subMenu then
				object.subMenu.y = object.parent.y + object.localPosition.y - 1
				object.subMenu.x = object.parent.x + object.parent.width
				if buffer.width - object.parent.x - object.parent.width + 1 < object.subMenu.width then
					object.subMenu.x = object.parent.x - object.subMenu.width
				end

				object.subMenu:show()
			else
				os.sleep(0.2)
			end

			object.pressed = false
			mainContainer:draw()
			buffer.draw()
			mainContainer.selectedItem = object:indexOf()

			GUI.callMethod(object.onTouch)
		end

		mainContainer:stopEventHandling()
	end
end

local function dropDownMenuCalculateSizes(menu)
	local totalHeight = 0
	for i = 1, #menu.itemsContainer.children do
		totalHeight = totalHeight + (menu.itemsContainer.children[i].type == GUI.dropDownMenuElementTypes.separator and 1 or menu.itemHeight)
		menu.itemsContainer.children[i].width = menu.width
	end
	menu.height = math.min(totalHeight, menu.maximumHeight, buffer.height - menu.y)
	menu.itemsContainer.width, menu.itemsContainer.height = menu.width, menu.height

	menu.nextButton.localPosition.y = menu.height
	menu.prevButton.width, menu.nextButton.width = menu.width, menu.width
	menu.prevButton.hidden = menu.itemsContainer.children[1].localPosition.y >= 1
	menu.nextButton.hidden = menu.itemsContainer.children[#menu.itemsContainer.children].localPosition.y + menu.itemsContainer.children[#menu.itemsContainer.children].height - 1 <= menu.height
end

local function dropDownMenuAddItem(menu, text, disabled, shortcut, color)
	local item = menu.itemsContainer:addChild(GUI.object(1, #menu.itemsContainer.children == 0 and 1 or menu.itemsContainer.children[#menu.itemsContainer.children].localPosition.y + menu.itemsContainer.children[#menu.itemsContainer.children].height, menu.width, menu.itemHeight))

	item.type = GUI.dropDownMenuElementTypes.default
	item.text = text
	item.disabled = disabled
	item.shortcut = shortcut
	item.color = color
	item.draw = dropDownMenuItemDraw
	item.eventHandler = dropDownMenuItemEventHandler

	dropDownMenuCalculateSizes(menu)

	return item
end

local function dropDownMenuAddSeparator(menu)
	local item = dropDownMenuAddItem(menu)
	item.type = GUI.dropDownMenuElementTypes.separator
	item.height = 1

	return item
end

local function dropDownMenuScrollDown(menu)
	if menu.itemsContainer.children[1].localPosition.y < 1 then
		for i = 1, #menu.itemsContainer.children do
			menu.itemsContainer.children[i].localPosition.y = menu.itemsContainer.children[i].localPosition.y + 1
		end
	end
	menu:draw()
	buffer.draw()
end

local function dropDownMenuScrollUp(menu)
	if menu.itemsContainer.children[#menu.itemsContainer.children].localPosition.y + menu.itemsContainer.children[#menu.itemsContainer.children].height - 1 > menu.height then
		for i = 1, #menu.itemsContainer.children do
			menu.itemsContainer.children[i].localPosition.y = menu.itemsContainer.children[i].localPosition.y - 1
		end
	end
	menu:draw()
	buffer.draw()
end

local function dropDownMenuEventHandler(mainContainer, object, eventData)
	if eventData[1] == "scroll" then
		if eventData[5] == 1 then
			dropDownMenuScrollDown(object)
		else
			dropDownMenuScrollUp(object)
		end
	end
end

local function dropDownMenuDraw(menu)
	dropDownMenuCalculateSizes(menu)

	if menu.oldPixels then
		buffer.paste(menu.x, menu.y, menu.oldPixels)
	else
		menu.oldPixels = buffer.copy(menu.x, menu.y, menu.width + 1, menu.height + 1)
	end

	buffer.square(menu.x, menu.y, menu.width, menu.height, menu.colors.default.background, menu.colors.default.text, " ", menu.colors.transparency.background)
	GUI.drawContainerContent(menu)
	GUI.windowShadow(menu.x, menu.y, menu.width, menu.height, menu.colors.transparency.shadow, true)

	return menu
end

local function dropDownMenuShow(menu)
	local mainContainer = GUI.fullScreenContainer()
	mainContainer:addChild(GUI.object(1, 1, mainContainer.width, mainContainer.height)).eventHandler = function(mainContainer, object, eventData)
		if eventData[1] == "touch" then
			mainContainer:stopEventHandling()
		end
	end
	mainContainer:addChild(menu)

	menu:draw()
	buffer.draw()
	mainContainer:startEventHandling()

	buffer.paste(menu.x, menu.y, menu.oldPixels)
	buffer.draw()
	if mainContainer.selectedItem then
		return menu.itemsContainer.children[mainContainer.selectedItem].text, mainContainer.selectedItem
	end
end

function GUI.dropDownMenu(x, y, width, maximumHeight, itemHeight, backgroundColor, textColor, backgroundPressedColor, textPressedColor, disabledColor, separatorColor, backgroundTransparency, shadowTransparency)
	local menu = GUI.container(x, y, width, 1)
	
	menu.colors = {
		default = {
			background = backgroundColor,
			text = textColor
		},
		pressed = {
			background = backgroundPressedColor,
			text = textPressedColor
		},
		disabled = {
			text = disabledColor
		},
		separator = separatorColor,
		transparency = {
			background = backgroundTransparency,
			shadow = shadowTransparency
		}
	}

	menu.itemsContainer = menu:addChild(GUI.container(1, 1, menu.width, menu.height))
	menu.prevButton = menu:addChild(GUI.button(1, 1, menu.width, 1, backgroundColor, textColor, backgroundPressedColor, textPressedColor, "▲"))
	menu.nextButton = menu:addChild(GUI.button(1, 1, menu.width, 1, backgroundColor, textColor, backgroundPressedColor, textPressedColor, "▼"))
	menu.prevButton.colors.default.transparency, menu.nextButton.colors.default.transparency = backgroundTransparency, backgroundTransparency
	menu.prevButton.onTouch = function()
		dropDownMenuScrollDown(menu)
	end
	menu.nextButton.onTouch = function()
		dropDownMenuScrollUp(menu)
	end

	menu.itemHeight = itemHeight
	menu.addSeparator = dropDownMenuAddSeparator
	menu.addItem = dropDownMenuAddItem
	menu.draw = dropDownMenuDraw
	menu.show = dropDownMenuShow
	menu.maximumHeight = maximumHeight
	menu.eventHandler = dropDownMenuEventHandler

	return menu
end

----------------------------------------- Context Menu -----------------------------------------

local function contextMenuCalculate(menu)
	local widestItem, widestShortcut = 0, 0
	for i = 1, #menu.itemsContainer.children do
		if menu.itemsContainer.children[i].type == GUI.dropDownMenuElementTypes.default then
			widestItem = math.max(widestItem, unicode.len(menu.itemsContainer.children[i].text))
			if menu.itemsContainer.children[i].shortcut then
				widestShortcut = math.max(widestShortcut, unicode.len(menu.itemsContainer.children[i].shortcut))
			end
		end
	end
	menu.width = 2 + widestItem + (widestShortcut > 0 and 3 + widestShortcut or 0)
	menu.height = #menu.itemsContainer.children
end

local function contextMenuShow(menu)
	contextMenuCalculate(menu)
	if menu.y + menu.height >= buffer.height then menu.y = buffer.height - menu.height end
	if menu.x + menu.width + 1 >= buffer.width then menu.x = buffer.width - menu.width - 1 end

	return dropDownMenuShow(menu)
end

local function contextMenuAddItem(menu, ...)
	contextMenuCalculate(menu)
	return dropDownMenuAddItem(menu, ...)
end

local function contextMenuAddSeparator(menu, ...)
	contextMenuCalculate(menu)
	return dropDownMenuAddSeparator(menu, ...)
end

local function contextMenuAddSubMenu(menu, text)
	local item = menu:addItem(text, false, "►")
	item.subMenu = GUI.contextMenu(1, 1)
	item.subMenu.colors = menu.colors
	
	return item.subMenu
end

function GUI.contextMenu(x, y, backgroundColor, textColor, backgroundPressedColor, textPressedColor, disabledColor, separatorColor, backgroundTransparency, shadowTransparency)
	local menu = GUI.dropDownMenu(x, y, 1, math.ceil(buffer.height * 0.5), 1,
		backgroundColor or GUI.colors.contextMenu.default.background,
		textColor or GUI.colors.contextMenu.default.text,
		backgroundPressedColor or GUI.colors.contextMenu.pressed.background,
		textPressedColor or GUI.colors.contextMenu.pressed.text,
		disabledColor or GUI.colors.contextMenu.disabled,
		separatorColor or GUI.colors.contextMenu.separator,
		backgroundTransparency or GUI.colors.contextMenu.transparency.background,
		shadowTransparency or GUI.colors.contextMenu.transparency.shadow
	)
	
	menu.colors.transparency.background = menu.colors.transparency.background or GUI.colors.contextMenu.transparency.background
	menu.colors.transparency.shadow = menu.colors.transparency.shadow or GUI.colors.contextMenu.transparency.shadow
	
	menu.show = contextMenuShow
	menu.addSubMenu = contextMenuAddSubMenu
	menu.addItem = contextMenuAddItem
	menu.addSeparator = contextMenuAddSeparator

	return menu
end

----------------------------------------- Combo Box Object -----------------------------------------

local function drawComboBox(object)
	buffer.square(object.x, object.y, object.width, object.height, object.colors.default.background, object.colors.default.text, " ")
	local x, y, limit, arrowSize = object.x + 1, math.floor(object.y + object.height / 2), object.width - 3, object.height
	if object.dropDownMenu.itemsContainer.children[object.selectedItem] then
		buffer.text(x, y, object.colors.default.text, string.limit(object.dropDownMenu.itemsContainer.children[object.selectedItem].text, limit, "right"))
	end
	GUI.button(object.x + object.width - arrowSize * 2 + 1, object.y, arrowSize * 2 - 1, arrowSize, object.colors.arrow.background, object.colors.arrow.text, 0x0, 0x0, object.pressed and "▲" or "▼"):draw()

	return object
end

local function comboBoxGetItem(object, index)
	return object.dropDownMenu.itemsContainer.children[index]
end

local function comboBoxClear(object)
	object.dropDownMenu.itemsContainer:deleteChildren()

	return object
end

local function comboBoxIndexOfItem(object, text)
	for i = 1, #object.dropDownMenu.itemsContainer.children do
		if object.dropDownMenu.itemsContainer.children[i].text == text then
			return i
		end
	end
end

local function selectComboBoxItem(object)
	object.pressed = true
	object:draw()

	object.dropDownMenu.x, object.dropDownMenu.y = object.x, object.y + object.height
	object.dropDownMenu.width = object.width
	local _, selectedItem = object.dropDownMenu:show()

	object.selectedItem = selectedItem or object.selectedItem
	object.pressed = false
	object:draw()
	buffer.draw()

	return object
end

local function comboBoxEventHandler(mainContainer, object, eventData)
	if eventData[1] == "touch" then
		object:selectItem()
		GUI.callMethod(object.onItemSelected, object.dropDownMenu.itemsContainer.children[object.selectedItem], eventData)
	end
end

local function comboBoxAddItem(object, ...)
	return object.dropDownMenu:addItem(...)
end

local function comboBoxAddSeparator(object)
	return object.dropDownMenu:addSeparator()
end

function GUI.comboBox(x, y, width, elementHeight, backgroundColor, textColor, arrowBackgroundColor, arrowTextColor)
	local object = GUI.object(x, y, width, elementHeight)
	
	object.eventHandler = comboBoxEventHandler
	object.colors = {
		default = {
			background = backgroundColor,
			text = textColor
		},
		pressed = {
			background = GUI.colors.contextMenu.pressed.background,
			text = GUI.colors.contextMenu.pressed.text
		},
		arrow = {
			background = arrowBackgroundColor,
			text = arrowTextColor
		}
	}

	object.dropDownMenu = GUI.dropDownMenu(1, 1, 1, math.ceil(buffer.height * 0.5), elementHeight,
		object.colors.default.background, 
		object.colors.default.text, 
		object.colors.pressed.background,
		object.colors.pressed.text,
		GUI.colors.contextMenu.disabled,
		GUI.colors.contextMenu.separator,
		GUI.colors.contextMenu.transparency.background, 
		GUI.colors.contextMenu.transparency.shadow
	)
	object.selectedItem = 1
	object.addItem = comboBoxAddItem
	object.addSeparator = comboBoxAddSeparator
	object.draw = drawComboBox
	object.selectItem = selectComboBoxItem
	object.clear = comboBoxClear
	object.indexOfItem = comboBoxIndexOfItem
	object.getItem = comboBoxGetItem

	return object
end

----------------------------------------- Switch and label object -----------------------------------------

local function switchAndLabelDraw(switchAndLabel)
	switchAndLabel.label.width = switchAndLabel.width
	switchAndLabel.switch.localPosition.x = switchAndLabel.width - switchAndLabel.switch.width
	GUI.calculateChildAbsolutePosition(switchAndLabel.label)
	GUI.calculateChildAbsolutePosition(switchAndLabel.switch)
	switchAndLabel.label:draw()
	switchAndLabel.switch:draw()

	return switchAndLabel
end

function GUI.switchAndLabel(x, y, width, switchWidth, activeColor, passiveColor, pipeColor, textColor, text, switchState)
	local switchAndLabel = GUI.container(x, y, width, 1)

	switchAndLabel.label = switchAndLabel:addChild(GUI.label(1, 1, width, 1, textColor, text))
	switchAndLabel.switch = switchAndLabel:addChild(GUI.switch(1, 1, switchWidth, activeColor, passiveColor, pipeColor, switchState))
	switchAndLabel.draw = switchAndLabelDraw

	return switchAndLabel 
end

----------------------------------------- Horizontal Slider Object -----------------------------------------

local function sliderDraw(object)
	-- На всякий случай делаем значение не меньше минимального и не больше максимального
	object.value = math.min(math.max(object.value, object.minimumValue), object.maximumValue)
	
	if object.showMaximumAndMinimumValues then
		local stringMaximumValue, stringMinimumValue = tostring(object.roundValues and math.floor(object.maximumValue) or math.roundToDecimalPlaces(object.maximumValue, 2)), tostring(object.roundValues and math.floor(object.minimumValue) or math.roundToDecimalPlaces(object.minimumValue, 2))
		buffer.text(object.x - unicode.len(stringMinimumValue) - 1, object.y, object.colors.value, stringMinimumValue)
		buffer.text(object.x + object.width + 1, object.y, object.colors.value, stringMaximumValue)
	end

	if object.currentValuePrefix or object.currentValuePostfix then
		local stringCurrentValue = (object.currentValuePrefix or "") .. (object.roundValues and math.floor(object.value) or math.roundToDecimalPlaces(object.value, 2)) .. (object.currentValuePostfix or "")
		buffer.text(math.floor(object.x + object.width / 2 - unicode.len(stringCurrentValue) / 2), object.y + 1, object.colors.value, stringCurrentValue)
	end

	local activeWidth = math.floor(object.width - ((object.maximumValue - object.value) * object.width / (object.maximumValue - object.minimumValue)))
	buffer.text(object.x, object.y, object.colors.passive, string.rep("━", object.width))
	buffer.text(object.x, object.y, object.colors.active, string.rep("━", activeWidth))
	buffer.text(object.x + activeWidth - 1, object.y, object.colors.pipe, "⬤")

	return object
end

local function sliderEventHandler(mainContainer, object, eventData)
	if eventData[1] == "touch" or eventData[1] == "drag" then
		local clickPosition = eventData[3] - object.x + 1
		object.value = object.minimumValue + (clickPosition * (object.maximumValue - object.minimumValue) / object.width)
		mainContainer:draw()
		buffer.draw()
		GUI.callMethod(object.onValueChanged, object.value, eventData)
	end
end

function GUI.slider(x, y, width, activeColor, passiveColor, pipeColor, valueColor, minimumValue, maximumValue, value, showMaximumAndMinimumValues, currentValuePrefix, currentValuePostfix)
	local object = GUI.object(x, y, width, 1)
	
	object.eventHandler = sliderEventHandler
	object.colors = {active = activeColor, passive = passiveColor, pipe = pipeColor, value = valueColor}
	object.draw = sliderDraw
	object.minimumValue = minimumValue
	object.maximumValue = maximumValue
	object.value = value
	object.showMaximumAndMinimumValues = showMaximumAndMinimumValues
	object.currentValuePrefix = currentValuePrefix
	object.currentValuePostfix = currentValuePostfix
	object.roundValues = false
	
	return object
end

----------------------------------------- Switch object -----------------------------------------

local function switchDraw(switch)
	buffer.text(switch.x - 1, switch.y, switch.colors.passive, "⠰")
	buffer.square(switch.x, switch.y, switch.width, 1, switch.colors.passive, 0x000000, " ")
	buffer.text(switch.x + switch.width, switch.y, switch.colors.passive, "⠆")

	buffer.text(switch.x - 1, switch.y, switch.colors.active, "⠰")
	buffer.square(switch.x, switch.y, switch.pipePosition - 1, 1, switch.colors.active, 0x000000, " ")

	buffer.text(switch.x + switch.pipePosition - 2, switch.y, switch.colors.pipe, "⠰")
	buffer.square(switch.x + switch.pipePosition - 1, switch.y, 2, 1, switch.colors.pipe, 0x000000, " ")
	buffer.text(switch.x + switch.pipePosition + 1, switch.y, switch.colors.pipe, "⠆")
	
	return switch
end

local function switchUpdate(switch)
	switch.pipePosition = switch.state and switch.width - 1 or 1
end

local function switchEventHandler(mainContainer, switch, eventData)
	if eventData[1] == "touch" then
		switch.state = not switch.state
		switch:addAnimation(
			function(mainContainer, switch, animation)
				if switch.state then
					switch.pipePosition = math.round(1 + animation.position * (switch.width - 2))
				else	
					switch.pipePosition = math.round(1 + (1 - animation.position) * (switch.width - 2))
				end
			end,
			function(mainContainer, switch, animation)
				animation:delete()
				GUI.callMethod(switch.onStateChanged)
			end
		):start(switch.animationDuration)
	end
end

function GUI.switch(x, y, width, activeColor, passiveColor, pipeColor, state)
	local switch = GUI.object(x, y, width, 1)

	switch.pipePosition = 1
	switch.eventHandler = switchEventHandler
	switch.colors = {
		active = activeColor,
		passive = passiveColor,
		pipe = pipeColor,
	}
	switch.draw = switchDraw
	switch.state = state or false
	switch.update = switchUpdate
	switch.animated = true
	switch.animationDuration = 0.3

	switch:update()
	
	return switch
end

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

local function brailleCanvasDraw(brailleCanvas)
	local index, background, foreground, symbol
	for y = 1, brailleCanvas.height do
		for x = 1, brailleCanvas.width do
			index = buffer.getIndexByCoordinates(brailleCanvas.x + x - 1, brailleCanvas.y + y - 1)
			background, foreground, symbol = buffer.rawGet(index)
			buffer.rawSet(index, background, brailleCanvas.pixels[y][x][9], brailleCanvas.pixels[y][x][10])
		end
	end

	return brailleCanvas
end

local function brailleCanvasSet(brailleCanvas, x, y, state, color)
	local xReal, yReal = math.ceil(x / 2), math.ceil(y / 4)
	if xReal <= brailleCanvas.width and yReal <= brailleCanvas.height then
		brailleCanvas.pixels[yReal][xReal][(y - (yReal - 1) * 4 - 1) * 2 + x - (xReal - 1) * 2] = state and 1 or 0
		brailleCanvas.pixels[yReal][xReal][9] = color
		brailleCanvas.pixels[yReal][xReal][10] = unicode.char(
			10240 +
			128 * brailleCanvas.pixels[yReal][xReal][8] +
			64 * brailleCanvas.pixels[yReal][xReal][7] +
			32 * brailleCanvas.pixels[yReal][xReal][6] +
			16 * brailleCanvas.pixels[yReal][xReal][4] +
			8 * brailleCanvas.pixels[yReal][xReal][2] +
			4 * brailleCanvas.pixels[yReal][xReal][5] +
			2 * brailleCanvas.pixels[yReal][xReal][3] +
			brailleCanvas.pixels[yReal][xReal][1]
		)
	end

	return brailleCanvas
end

function GUI.brailleCanvas(x, y, width, height)
	local brailleCanvas = GUI.object(x, y, width, height)
	
	brailleCanvas.pixels = {}
	brailleCanvas.set = brailleCanvasSet
	brailleCanvas.draw = brailleCanvasDraw

	for j = 1, height * 4 do
		brailleCanvas.pixels[j] = {}
		for i = 1, width * 2 do
			brailleCanvas.pixels[j][i] = { 0, 0, 0, 0, 0, 0, 0, 0, 0x0, " " }
		end
	end

	return brailleCanvas
end

----------------------------------------- Layout object -----------------------------------------

local function layoutCheckCell(layout, column, row)
	if column < 1 or column > #layout.grid.columnSizes or row < 1 or row > #layout.grid.rowSizes then
		error("Specified grid position (" .. tostring(column) .. "x" .. tostring(row) .. ") is out of layout grid range")
	end
end

local function layoutGetAbsoluteTotalSize(array)
	local absoluteTotalSize = 0
	for i = 1, #array do
		if array[i].sizePolicy == GUI.sizePolicies.absolute then
			absoluteTotalSize = absoluteTotalSize + array[i].size
		end
	end
	return absoluteTotalSize
end

local function layoutGetCalculatedSize(array, index, dependency)
	if array[index].sizePolicy == GUI.sizePolicies.percentage then
		array[index].calculatedSize = array[index].size * dependency
	else
		array[index].calculatedSize = array[index].size
	end
end

local function layoutUpdate(layout)
	local columnPercentageTotalSize, rowPercentageTotalSize = layout.width - layoutGetAbsoluteTotalSize(layout.grid.columnSizes), layout.height - layoutGetAbsoluteTotalSize(layout.grid.rowSizes)
	for row = 1, #layout.grid.rowSizes do
		layoutGetCalculatedSize(layout.grid.rowSizes, row, rowPercentageTotalSize)
		for column = 1, #layout.grid.columnSizes do
			layoutGetCalculatedSize(layout.grid.columnSizes, column, columnPercentageTotalSize)
			layout.grid[row][column].totalWidth, layout.grid[row][column].totalHeight = 0, 0
		end
	end

	-- Подготавливаем объекты к расположению и подсчитываем тотальные размеры
	for i = 1, #layout.children do
		if not layout.children[i].hidden then
			-- Проверка на позицию в сетке
			if layout.children[i].layoutGridPosition.row >= 1 and layout.children[i].layoutGridPosition.row <= #layout.grid.rowSizes and layout.children[i].layoutGridPosition.column >= 1 and layout.children[i].layoutGridPosition.column <= #layout.grid.columnSizes then
				-- Авто-фиттинг объектов
				if layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].fitting.horizontal then
					layout.children[i].width = math.round(layout.grid.columnSizes[layout.children[i].layoutGridPosition.column].calculatedSize - layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].fitting.horizontalRemove)
				end
				if layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].fitting.vertical then
					layout.children[i].height = math.round(layout.grid.rowSizes[layout.children[i].layoutGridPosition.row].calculatedSize - layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].fitting.verticalRemove)
				end

				-- Направление и расчет размеров
				if layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].direction == GUI.directions.horizontal then
					layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].totalWidth = layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].totalWidth + layout.children[i].width + layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].spacing
					layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].totalHeight = math.max(layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].totalHeight, layout.children[i].height)
				else
					layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].totalWidth = math.max(layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].totalWidth, layout.children[i].width)
					layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].totalHeight = layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].totalHeight + layout.children[i].height + layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].spacing
				end
			else
				error("Layout child with index " .. i .. " has been assigned to cell (" .. layout.children[i].layoutGridPosition.column .. "x" .. layout.children[i].layoutGridPosition.row .. ") out of layout grid range")
			end
		end
	end

	-- Высчитываем позицицию объектов
	local x, y = 1, 1
	for row = 1, #layout.grid.rowSizes do
		for column = 1, #layout.grid.columnSizes do
			layout.grid[row][column].x, layout.grid[row][column].y = GUI.getAlignmentCoordinates(
				{
					x = x,
					y = y,
					width = layout.grid.columnSizes[column].calculatedSize,
					height = layout.grid.rowSizes[row].calculatedSize,
					alignment = layout.grid[row][column].alignment,
				},
				{
					width = layout.grid[row][column].totalWidth - (layout.grid[row][column].direction == GUI.directions.horizontal and layout.grid[row][column].spacing or 0),
					height = layout.grid[row][column].totalHeight - (layout.grid[row][column].direction == GUI.directions.vertical and layout.grid[row][column].spacing or 0),
				}
			)
			if layout.grid[row][column].margin then
				layout.grid[row][column].x, layout.grid[row][column].y = GUI.getMarginCoordinates(layout.grid[row][column])
			end

			x = x + layout.grid.columnSizes[column].calculatedSize
		end

		x, y = 1, y + layout.grid.rowSizes[row].calculatedSize
	end

	-- Размещаем все объекты
	for i = 1, #layout.children do
		if not layout.children[i].hidden then
			if layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].direction == GUI.directions.horizontal then
				layout.children[i].localPosition.x = math.round(layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].x)
				layout.children[i].localPosition.y = math.round(layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].y + layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].totalHeight / 2 - layout.children[i].height / 2)
				
				layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].x = layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].x + layout.children[i].width + layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].spacing
			else
				layout.children[i].localPosition.x = math.round(layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].x + layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].totalWidth / 2 - layout.children[i].width / 2)
				layout.children[i].localPosition.y = math.round(layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].y)
				
				layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].y = layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].y + layout.children[i].height + layout.grid[layout.children[i].layoutGridPosition.row][layout.children[i].layoutGridPosition.column].spacing
			end
		end
	end
end

local function layoutSetCellPosition(layout, column, row, object)
	layoutCheckCell(layout, column, row)
	object.layoutGridPosition = {column = column, row = row}

	return object
end

local function layoutSetCellDirection(layout, column, row, direction)
	layoutCheckCell(layout, column, row)
	layout.grid[row][column].direction = direction

	return layout
end

local function layoutSetCellSpacing(layout, column, row, spacing)
	layoutCheckCell(layout, column, row)
	layout.grid[row][column].spacing = spacing

	return layout
end

local function layoutSetCellAlignment(layout, column, row, horizontalAlignment, verticalAlignment)
	layoutCheckCell(layout, column, row)
	layout.grid[row][column].alignment.horizontal, layout.grid[row][column].alignment.vertical = horizontalAlignment, verticalAlignment

	return layout
end

local function layoutSetCellMargin(layout, column, row, horizontalMargin, verticalMargin)
	layoutCheckCell(layout, column, row)
	layout.grid[row][column].margin = {
		horizontal = horizontalMargin,
		vertical = verticalMargin
	}

	return layout
end

local function layoutNewCell()
	return {
		alignment = {
			horizontal = GUI.alignment.horizontal.center,
			vertical = GUI.alignment.vertical.center
		},
		direction = GUI.directions.vertical,
		fitting = {
		horizontal = false, vertical = false},
		spacing = 1,
	}
end

local function layoutCalculatePercentageSize(changingExistent, array, index)
	if array[index].sizePolicy == GUI.sizePolicies.percentage then
		local allPercents, beforeFromIndexPercents = 0, 0
		for i = 1, #array do
			if array[i].sizePolicy == GUI.sizePolicies.percentage then
				allPercents = allPercents + array[i].size

				if i <= index then
					beforeFromIndexPercents = beforeFromIndexPercents + array[i].size
				end
			end
		end

		local modifyer
		if changingExistent then
			if beforeFromIndexPercents > 1 then
				error("Layout summary percentage > 100% at index " .. index)
			end
			modifyer = (1 - beforeFromIndexPercents) / (allPercents - beforeFromIndexPercents)
		else
			modifyer = (1 - array[index].size) / (allPercents - array[index].size)
		end

		for i = changingExistent and index + 1 or 1, #array do
			if array[i].sizePolicy == GUI.sizePolicies.percentage and i ~= index then
				array[i].size = modifyer * array[i].size
			end
		end
	end
end

local function layoutSetColumnWidth(layout, column, sizePolicy, size)
	layout.grid.columnSizes[column].sizePolicy, layout.grid.columnSizes[column].size = sizePolicy, size
	layoutCalculatePercentageSize(true, layout.grid.columnSizes, column)

	return layout
end

local function layoutSetRowHeight(layout, row, sizePolicy, size)
	layout.grid.rowSizes[row].sizePolicy, layout.grid.rowSizes[row].size = sizePolicy, size
	layoutCalculatePercentageSize(true, layout.grid.rowSizes, row)

	return layout
end

local function layoutAddColumn(layout, sizePolicy, size)
	for i = 1, #layout.grid.rowSizes do
		table.insert(layout.grid[i], layoutNewCell())
	end

	table.insert(layout.grid.columnSizes, {
		sizePolicy = sizePolicy,
		size = size
	})
	layoutCalculatePercentageSize(false, layout.grid.columnSizes, #layout.grid.columnSizes)
	-- GUI.error(layout.grid.columnSizes)

	return layout
end

local function layoutAddRow(layout, sizePolicy, size)
	local row = {}
	for i = 1, #layout.grid.columnSizes do
		table.insert(row, layoutNewCell())
	end

	table.insert(layout.grid, row)
	table.insert(layout.grid.rowSizes, {
		sizePolicy = sizePolicy,
		size = size
	})

	layoutCalculatePercentageSize(false, layout.grid.rowSizes, #layout.grid.rowSizes)
	-- GUI.error(layout.grid.rowSizes)

	return layout
end

local function layoutRemoveRow(layout, row)
	table.remove(layout.grid, row)

	layout.grid.rowSizes[row].size = 0
	layoutCalculatePercentageSize(false, layout.grid.rowSizes, row)

	table.remove(layout.grid.rowSizes, row)

	return layout
end

local function layoutRemoveColumn(layout, column)
	for i = 1, #layout.grid.rowSizes do
		table.remove(layout.grid[i], column)
	end

	layout.grid.columnSizes[column].size = 0
	layoutCalculatePercentageSize(false, layout.grid.columnSizes, column)

	table.remove(layout.grid.columnSizes, column)

	return layout
end

local function layoutSetGridSize(layout, columnCount, rowCount)
	layout.grid = {
		rowSizes = {},
		columnSizes = {}
	}

	local rowSize, columnSize = 1 / rowCount, 1 / columnCount
	for i = 1, rowCount do
		layoutAddRow(layout, GUI.sizePolicies.percentage, 1 / i)
	end

	for i = 1, columnCount do
		layoutAddColumn(layout, GUI.sizePolicies.percentage, 1 / i)
	end

	return layout
end

local function layoutDraw(layout)
	layoutUpdate(layout)
	GUI.drawContainerContent(layout)
	
	if layout.showGrid then
		local x, y = layout.x, layout.y
		for j = 1, #layout.grid.columnSizes do
			for i = 1, #layout.grid.rowSizes do
				buffer.frame(
					math.round(x),
					math.round(y),
					math.round(layout.grid.columnSizes[j].calculatedSize),
					math.round(layout.grid.rowSizes[i].calculatedSize),
					0xFF0000
				)
				y = y + layout.grid.rowSizes[i].calculatedSize
			end
			x, y = x + layout.grid.columnSizes[j].calculatedSize, layout.y
		end
	end
end

local function layoutFitToChildrenSize(layout, column, row)
	layout.width, layout.height = 0, 0

	for i = 1, #layout.children do
		if not layout.children[i].hidden then
			if layout.grid[row][column].direction == GUI.directions.horizontal then
				layout.width = layout.width + layout.children[i].width + layout.grid[row][column].spacing
				layout.height = math.max(layout.height, layout.children[i].height)
			else
				layout.width = math.max(layout.width, layout.children[i].width)
				layout.height = layout.height + layout.children[i].height + layout.grid[row][column].spacing
			end
		end
	end

	if layout.grid[row][column].direction == GUI.directions.horizontal then
		layout.width = layout.width - layout.grid[row][column].spacing
	else
		layout.height = layout.height - layout.grid[row][column].spacing
	end

	return layout
end

local function layoutSetCellFitting(layout, column, row, horizontal, vertical, horizontalRemove, verticalRemove )
	layoutCheckCell(layout, column, row)
	layout.grid[row][column].fitting = {
		horizontal = horizontal,
		vertical = vertical,
		horizontalRemove = horizontalRemove or 0,
		verticalRemove = verticalRemove or 0,
	}

	return layout
end

local function layoutAddChild(layout, object, ...)
	GUI.addChildToContainer(layout, object, ...).layoutGridPosition = {
		column = layout.defaultColumn,
		row = layout.defaultRow
	}

	return object
end

function GUI.layout(x, y, width, height, columnCount, rowCount)
	local layout = GUI.container(x, y, width, height)

	layout.defaultRow = 1
	layout.defaultColumn = 1

	layout.addRow = layoutAddRow
	layout.addColumn = layoutAddColumn
	layout.removeRow = layoutRemoveRow
	layout.removeColumn = layoutRemoveColumn

	layout.setRowHeight = layoutSetRowHeight
	layout.setColumnWidth = layoutSetColumnWidth

	layout.setCellPosition = layoutSetCellPosition
	layout.setCellDirection = layoutSetCellDirection
	layout.setGridSize = layoutSetGridSize
	layout.setCellSpacing = layoutSetCellSpacing
	layout.setCellAlignment = layoutSetCellAlignment
	layout.setCellMargin = layoutSetCellMargin
	
	layout.fitToChildrenSize = layoutFitToChildrenSize
	layout.setCellFitting = layoutSetCellFitting

	layout.addChild = layoutAddChild
	layout.draw = layoutDraw

	layoutSetGridSize(layout, columnCount, rowCount)

	return layout
end

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

local function filesystemDialogDraw(filesystemDialog)
	if filesystemDialog.extensionComboBox.hidden then
		filesystemDialog.input.width = filesystemDialog.cancelButton.localPosition.x - 4
	else
		filesystemDialog.input.width = filesystemDialog.extensionComboBox.localPosition.x - 3
	end

	if filesystemDialog.IOMode == GUI.filesystemModes.save then
		filesystemDialog.submitButton.disabled = not filesystemDialog.input.text
	else
		filesystemDialog.input.text = filesystemDialog.filesystemTree.selectedItem or ""
		filesystemDialog.submitButton.disabled = not filesystemDialog.filesystemTree.selectedItem
	end
	
	GUI.drawContainerContent(filesystemDialog)
	GUI.windowShadow(filesystemDialog.x, filesystemDialog.y, filesystemDialog.width, filesystemDialog.height, GUI.colors.contextMenu.transparency.shadow, true)

	return filesystemDialog
end

local function filesystemDialogSetMode(filesystemDialog, IOMode, filesystemMode)
	filesystemDialog.IOMode = IOMode
	filesystemDialog.filesystemMode = filesystemMode

	if filesystemDialog.IOMode == GUI.filesystemModes.save then
		filesystemDialog.filesystemTree.showMode = GUI.filesystemModes.directory
		filesystemDialog.filesystemTree.selectionMode = GUI.filesystemModes.directory
		filesystemDialog.input.eventHandler = inputEventHandler
		filesystemDialog.extensionComboBox.hidden = not (filesystemDialog.filesystemMode == GUI.filesystemModes.file)
	else
		if filesystemDialog.filesystemMode == GUI.filesystemModes.file then
			filesystemDialog.filesystemTree.showMode = GUI.filesystemModes.both
			filesystemDialog.filesystemTree.selectionMode = GUI.filesystemModes.file
		else
			filesystemDialog.filesystemTree.showMode = GUI.filesystemModes.directory
			filesystemDialog.filesystemTree.selectionMode = GUI.filesystemModes.directory
		end

		filesystemDialog.input.eventHandler = nil
		filesystemDialog.extensionComboBox.hidden = true
	end

	filesystemDialog.filesystemTree:updateFileList()
end

local function filesystemDialogAddExtensionFilter(filesystemDialog, extension)
	filesystemDialog.extensionComboBox:addItem(extension)
	filesystemDialog.extensionComboBox.width = math.max(filesystemDialog.extensionComboBox.width, unicode.len(extension) + 3)
	filesystemDialog.extensionComboBox.localPosition.x = filesystemDialog.cancelButton.localPosition.x - filesystemDialog.extensionComboBox.width - 2

	filesystemDialog.filesystemTree:addExtensionFilter(extension)
end

function GUI.filesystemDialog(x, y, width, height, submitButtonText, cancelButtonText, placeholderText, path)
	local filesystemDialog = GUI.container(x, y, width, height)
	
	filesystemDialog:addChild(GUI.panel(1, height - 2, width, 3, 0xD2D2D2))
	
	filesystemDialog.cancelButton = filesystemDialog:addChild(GUI.adaptiveRoundedButton(1, height - 1, 2, 0, 0xE1E1E1, 0x3C3C3C, 0x3C3C3C, 0xE1E1E1, cancelButtonText))
	filesystemDialog.submitButton = filesystemDialog:addChild(GUI.adaptiveRoundedButton(1, height - 1, 2, 0, 0x3C3C3C, 0xE1E1E1, 0xE1E1E1, 0x3C3C3C, submitButtonText))
	filesystemDialog.submitButton.localPosition.x = filesystemDialog.width - filesystemDialog.submitButton.width - 1
	filesystemDialog.cancelButton.localPosition.x = filesystemDialog.submitButton.localPosition.x - filesystemDialog.cancelButton.width - 2

	filesystemDialog.extensionComboBox = filesystemDialog:addChild(GUI.comboBox(1, height - 1, 1, 1, 0xE1E1E1, 0x555555, 0xC3C3C3, 0x888888))
	filesystemDialog.extensionComboBox.hidden = true

	filesystemDialog.input = filesystemDialog:addChild(GUI.input(2, height - 1, 1, 1, 0xE1E1E1, 0x555555, 0x888888, 0xE1E1E1, 0x777777, "", placeholderText))

	filesystemDialog.filesystemTree = filesystemDialog:addChild(GUI.filesystemTree(1, 1, width, height - 3, 0xE1E1E1, 0x3C3C3C, 0x3C3C3C, 0xAAAAAA, 0x3C3C3C, 0xE1E1E1, 0xBBBBBB, 0xAAAAAA, 0xC3C3C3, 0x444444))
	filesystemDialog.filesystemTree.workPath = path

	filesystemDialog.draw = filesystemDialogDraw
	filesystemDialog.setMode = filesystemDialogSetMode
	filesystemDialog.addExtensionFilter = filesystemDialogAddExtensionFilter

	filesystemDialog:setMode(GUI.filesystemModes.open, GUI.filesystemModes.file)

	return filesystemDialog
end

local function filesystemDialogShow(filesystemDialog)
	filesystemDialog:addAnimation(
		function(mainContainer, object, animation)
			object.localPosition.y = math.floor(1 + (1.0 - animation.position) * (-object.height))
		end,
		function(mainContainer, switch, animation)
			animation:delete()
		end
	):start(0.5)

	return filesystemDialog
end

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

function GUI.addFilesystemDialogToContainer(parentContainer, ...)
	local container = parentContainer:addChild(GUI.container(1, 1, parentContainer.width, parentContainer.height))
	container:addChild(GUI.object(1, 1, container.width, container.height))
	
	local filesystemDialog = container:addChild(GUI.filesystemDialog(1, 1, math.floor(container.width * 0.35), math.floor(container.height * 0.8), ...))
	filesystemDialog.localPosition.x = math.floor(container.width / 2 - filesystemDialog.width / 2)
	filesystemDialog.localPosition.y = -filesystemDialog.height

	local function onAnyTouch()
		local firstParent = filesystemDialog:getFirstParent()
		container:delete()
		firstParent:draw()
		buffer.draw()
	end

	filesystemDialog.cancelButton.onTouch = function()
		onAnyTouch()
		GUI.callMethod(filesystemDialog.onCancel)
	end

	filesystemDialog.submitButton.onTouch = function()
		onAnyTouch()
		
		local path = filesystemDialog.filesystemTree.selectedItem or filesystemDialog.filesystemTree.workPath or "/"
		if filesystemDialog.IOMode == GUI.filesystemModes.save then
			path = path .. filesystemDialog.input.text
			
			if filesystemDialog.filesystemMode == GUI.filesystemModes.file then
				path = path .. filesystemDialog.extensionComboBox:getItem(filesystemDialog.extensionComboBox.selectedItem).text
			else
				path = path .. "/"
			end
		end

		GUI.callMethod(filesystemDialog.onSubmit, path)
	end

	filesystemDialog.show = filesystemDialogShow

	return filesystemDialog
end

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

local function filesystemChooserDraw(object)
	local tipWidth = object.height * 2 - 1
	local y = math.floor(object.y + object.height / 2)
	
	buffer.square(object.x, object.y, object.width - tipWidth, object.height, object.colors.background, object.colors.text, " ")
	buffer.square(object.x + object.width - tipWidth, object.y, tipWidth, object.height, object.pressed and object.colors.tipText or object.colors.tipBackground, object.pressed and object.colors.tipBackground or object.colors.tipText, " ")
	buffer.text(object.x + object.width - math.floor(tipWidth / 2) - 1, y, object.pressed and object.colors.tipBackground or object.colors.tipText, "…")
	buffer.text(object.x + 1, y, object.colors.text, string.limit(object.path or object.placeholderText, object.width - tipWidth - 2, "left"))

	return filesystemChooser
end

local function filesystemChooserAddExtensionFilter(object, extension)
	object.extensionFilters[unicode.lower(extension)] = true
end

local function filesystemChooserSetMode(object, filesystemMode)
	object.filesystemMode = filesystemMode
end

local function filesystemChooserEventHandler(mainContainer, object, eventData)
	if eventData[1] == "touch" then
		object.pressed = true
		mainContainer:draw()
		buffer.draw()

		local filesystemDialog = GUI.addFilesystemDialogToContainer(mainContainer, object.submitButtonText, object.cancelButtonText, object.placeholderText, object.filesystemDialogPath)		
		
		for key in pairs(object.extensionFilters) do
			filesystemDialog:addExtensionFilter(key)
		end
		filesystemDialog:setMode(GUI.filesystemModes.open, object.filesystemMode)
		
		filesystemDialog.onCancel = function()
			object.pressed = false

			mainContainer:draw()
			buffer.draw()
		end

		filesystemDialog.onSubmit = function(path)
			object.path = path
			object.pressed = false

			mainContainer:draw()
			buffer.draw()
			GUI.callMethod(object.onSubmit, object.path)
		end

		filesystemDialog:show()
	end
end

function GUI.filesystemChooser(x, y, width, height, backgroundColor, textColor, tipBackgroundColor, tipTextColor, path, submitButtonText, cancelButtonText, placeholderText, filesystemDialogPath)
	local object = GUI.object(x, y, width, height)
	
	object.eventHandler = comboBoxEventHandler
	object.colors = {
		tipBackground = tipBackgroundColor,
		tipText = tipTextColor,
		text = textColor,
		background = backgroundColor
	}

	object.submitButtonText = submitButtonText
	object.cancelButtonText = cancelButtonText
	object.placeholderText = placeholderText
	object.pressed = false
	object.path = path
	object.filesystemDialogPath = filesystemDialogPath
	object.filesystemMode = GUI.filesystemModes.file
	object.extensionFilters = {}

	object.draw = filesystemChooserDraw
	object.eventHandler = filesystemChooserEventHandler
	object.addExtensionFilter = filesystemChooserAddExtensionFilter
	object.setMode = filesystemChooserSetMode

	return object
end

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

local function resizerDraw(object)
	local horizontalMode = object.width >= object.height
	local x, y, symbol
	if horizontalMode then
		buffer.text(object.x, math.floor(object.y + object.height / 2), object.colors.helper, string.rep("━", object.width))
	else
		local x = math.floor(object.x + object.width / 2)
		for i = object.y, object.y + object.height - 1 do
			local index = buffer.getIndexByCoordinates(x, i)
			buffer.rawSet(index, buffer.rawGet(index), object.colors.helper, "┃")
		end
	end

	if object.touchPosition then
		buffer.text(object.touchPosition.x - 1, object.touchPosition.y, object.colors.arrow, "←→")
	end
end

local function resizerEventHandler(mainContainer, object, eventData)
	if eventData[1] == "touch" then
		object.touchPosition = {x = eventData[3], y = eventData[4]}
		
		mainContainer:draw()
		buffer.draw()
	elseif eventData[1] == "drag" and object.touchPosition then
		local x, y = object.touchPosition.x, object.touchPosition.y
		object.touchPosition.x, object.touchPosition.y = eventData[3], eventData[4]
		
		if object.onResize then
			object.onResize(eventData[3] - x, eventData[4] - y)
		end

		mainContainer:draw()
		buffer.draw()
	elseif eventData[1] == "drop" then
		object.touchPosition = nil

		if object.onResizeFinished then
			object.onResizeFinished()
		end

		mainContainer:draw()
		buffer.draw()
	end
end

function GUI.resizer(x, y, width, height, helperColor, arrowColor)
	local object = GUI.object(x, y, width, height)
	
	object.colors = {
		helper = helperColor,
		arrow = arrowColor
	}

	object.draw = resizerDraw
	object.eventHandler = resizerEventHandler

	return object
end

----------------------------------------- Scrollbar object -----------------------------------------

local function scrollBarDraw(scrollBar)
	local isVertical = scrollBar.height > scrollBar.width
	local valuesDelta = scrollBar.maximumValue - scrollBar.minimumValue + 1
	local part = scrollBar.value / valuesDelta

	if isVertical then
		local barSize = math.ceil(scrollBar.shownValueCount / valuesDelta * scrollBar.height)
		local halfBarSize = math.floor(barSize / 2)
		
		scrollBar.ghostPosition.y = scrollBar.y + halfBarSize
		scrollBar.ghostPosition.height = scrollBar.height - barSize

		if scrollBar.thin then
			local y1 = math.floor(scrollBar.ghostPosition.y + part * scrollBar.ghostPosition.height - halfBarSize)
			local y2 = y1 + barSize - 1
			local background

			for y = scrollBar.y, scrollBar.y + scrollBar.height - 1 do
				background = buffer.get(scrollBar.x, y)
				buffer.set(scrollBar.x, y, background, y >= y1 and y <= y2 and scrollBar.colors.foreground or scrollBar.colors.background, "┃")
			end
		else
			buffer.square(scrollBar.x, scrollBar.y, scrollBar.width, scrollBar.height, scrollBar.colors.background, scrollBar.colors.foreground, " ")
			buffer.square(
				scrollBar.x,
				math.floor(scrollBar.ghostPosition.y + part * scrollBar.ghostPosition.height - halfBarSize),
				scrollBar.width,
				barSize,
				scrollBar.colors.foreground, 0x0, " "
			)
		end
	else
		local barSize = math.ceil(scrollBar.shownValueCount / valuesDelta * scrollBar.width)
		local halfBarSize = math.floor(barSize / 2)
		
		scrollBar.ghostPosition.x = scrollBar.x + halfBarSize
		scrollBar.ghostPosition.width = scrollBar.width - barSize

		if scrollBar.thin then
			local x1 = math.floor(scrollBar.ghostPosition.x + part * scrollBar.ghostPosition.width - halfBarSize)
			local x2 = x1 + barSize - 1
			local background

			for x = scrollBar.x, scrollBar.x + scrollBar.width - 1 do
				background = buffer.get(x, scrollBar.y)
				buffer.set(x, scrollBar.y, background, x >= x1 and x <= x2 and scrollBar.colors.foreground or scrollBar.colors.background, "⠤")
			end
		else
			buffer.square(scrollBar.x, scrollBar.y, scrollBar.width, scrollBar.height, scrollBar.colors.background, scrollBar.colors.foreground, " ")
			buffer.square(
				math.floor(scrollBar.ghostPosition.x + part * scrollBar.ghostPosition.width - halfBarSize),
				scrollBar.y,
				barSize,
				scrollBar.height,
				scrollBar.colors.foreground, 0x0, " "
			)
		end
	end

	return scrollBar
end

local function scrollBarEventHandler(mainContainer, object, eventData)
	local newValue = object.value

	if eventData[1] == "touch" or eventData[1] == "drag" then
		local delta = object.maximumValue - object.minimumValue + 1
		if object.height > object.width then
			newValue = math.floor((eventData[4] - object.y + 1) / object.height * delta)
		else
			newValue = math.floor((eventData[3] - object.x + 1) / object.width * delta)
		end
	elseif eventData[1] == "scroll" then
		if eventData[5] == 1 then
			if object.value >= object.minimumValue + object.onScrollValueIncrement then
				newValue = object.value - object.onScrollValueIncrement
			else
				newValue = object.minimumValue
			end
		else
			if object.value <= object.maximumValue - object.onScrollValueIncrement then
				newValue = object.value + object.onScrollValueIncrement
			else
				newValue = object.maximumValue
			end
		end
	end

	if eventData[1] == "touch" or eventData[1] == "drag" or eventData[1] == "scroll" then
		object.value = newValue
		GUI.callMethod(object.onTouch, eventData)
		mainContainer:draw()
		buffer.draw()
	end
end

function GUI.scrollBar(x, y, width, height, backgroundColor, foregroundColor, minimumValue, maximumValue, value, shownValueCount, onScrollValueIncrement, thin)
	local scrollBar = GUI.object(x, y, width, height)

	scrollBar.eventHandler = scrollBarEventHandler
	scrollBar.maximumValue = maximumValue
	scrollBar.minimumValue = minimumValue
	scrollBar.value = value
	scrollBar.onScrollValueIncrement = onScrollValueIncrement
	scrollBar.shownValueCount = shownValueCount
	scrollBar.thin = thin
	scrollBar.colors = {
		background = backgroundColor,
		foreground = foregroundColor,
	}
	scrollBar.ghostPosition = {}
	scrollBar.draw = scrollBarDraw

	return scrollBar
end

----------------------------------------- Tree object -----------------------------------------

local function treeDraw(tree)	
	local y, yEnd, showScrollBar = tree.y, tree.y + tree.height - 1, #tree.items > tree.height
	local textLimit = tree.width - (showScrollBar and 1 or 0)

	if tree.colors.default.background then
		buffer.square(tree.x, tree.y, tree.width, tree.height, tree.colors.default.background, tree.colors.default.expandable, " ")
	end

	for i = tree.fromItem, #tree.items do
		local textColor, arrowColor, text = tree.colors.default.notExpandable, tree.colors.default.arrow, tree.items[i].expandable and "■ " or "□ "

		if tree.selectedItem == tree.items[i].definition then
			textColor, arrowColor = tree.colors.selected.any, tree.colors.selected.arrow
			buffer.square(tree.x, y, tree.width, 1, tree.colors.selected.background, textColor, " ")
		else
			if tree.items[i].expandable then
				textColor = tree.colors.default.expandable
			elseif tree.items[i].disabled then
				textColor = tree.colors.disabled
			end
		end

		if tree.items[i].expandable then
			buffer.text(tree.x + tree.items[i].offset, y, arrowColor, tree.expandedItems[tree.items[i].definition] and "▽" or "▷")
		end

		buffer.text(tree.x + tree.items[i].offset + 2, y, textColor, unicode.sub(text .. fs.name(tree.items[i].name), 1, textLimit - tree.items[i].offset - 2))

		y = y + 1
		if y > yEnd then break end
	end

	if showScrollBar then
		local scrollBar = tree.scrollBar
		scrollBar.x = tree.x + tree.width - 1
		scrollBar.y = tree.y
		scrollBar.width = 1
		scrollBar.height = tree.height
		scrollBar.colors.background = tree.colors.scrollBar.background
		scrollBar.colors.foreground = tree.colors.scrollBar.foreground
		scrollBar.minimumValue = 1
		scrollBar.maximumValue = #tree.items
		scrollBar.value = tree.fromItem
		scrollBar.shownValueCount = tree.height
		scrollBar.onScrollValueIncrement = 1
		scrollBar.thin = true

		scrollBar:draw()
	end

	return tree
end

local function treeEventHandler(mainContainer, tree, eventData)
	if eventData[1] == "touch" then
		local i = eventData[4] - tree.y + tree.fromItem
		if tree.items[i] then
			if
				tree.items[i].expandable and
				(
					tree.selectionMode == GUI.filesystemModes.file or
					eventData[3] >= tree.x + tree.items[i].offset - 1 and eventData[3] <= tree.x + tree.items[i].offset + 1
				)
			then
				if tree.expandedItems[tree.items[i].definition] then
					tree.expandedItems[tree.items[i].definition] = nil
				else
					tree.expandedItems[tree.items[i].definition] = true
				end

				GUI.callMethod(tree.onItemExpanded, tree.selectedItem, eventData)
			else
				if
					(
						(
							tree.selectionMode == GUI.filesystemModes.both or
							tree.selectionMode == GUI.filesystemModes.directory and tree.items[i].expandable or
							tree.selectionMode == GUI.filesystemModes.file
						) and not tree.items[i].disabled
					)
				then
					tree.selectedItem = tree.items[i].definition
					GUI.callMethod(tree.onItemSelected, tree.selectedItem, eventData)
				end
			end

			mainContainer:draw()
			buffer.draw()
		end
	elseif eventData[1] == "scroll" then
		if eventData[5] == 1 then
			if tree.fromItem > 1 then
				tree.fromItem = tree.fromItem - 1
				mainContainer:draw()
				buffer.draw()
			end
		else
			if tree.fromItem < #tree.items then
				tree.fromItem = tree.fromItem + 1
				mainContainer:draw()
				buffer.draw()
			end
		end
	end
end

local function treeAddItem(tree, name, definition, offset, expandable, disabled)
	table.insert(tree.items, {name = name, expandable = expandable, offset = offset or 0, definition = definition, disabled = disabled})
	return tree
end

function GUI.tree(x, y, width, height, backgroundColor, expandableColor, notExpandableColor, arrowColor, backgroundSelectedColor, anySelectionColor, arrowSelectionColor, disabledColor, scrollBarBackground, scrollBarForeground, showMode, selectionMode)
	local tree = GUI.container(x, y, width, height)
	
	tree.eventHandler = treeEventHandler
	tree.colors = {
		default = {
			background = backgroundColor,
			expandable = expandableColor,
			notExpandable = notExpandableColor,
			arrow = arrowColor,
		},
		selected = {
			background = backgroundSelectedColor,
			any = anySelectionColor,
			arrow = arrowSelectionColor,
		},
		scrollBar = {
			background = scrollBarBackground,
			foreground = scrollBarForeground
		},
		disabled = disabledColor
	}
	tree.items = {}
	tree.fromItem = 1
	tree.selectedItem = nil
	tree.expandedItems = {}

	tree.scrollBar = GUI.scrollBar(1, 1, 1, 1, 0x0, 0x0, 1, 1, 1, 1, 1)

	tree.showMode = showMode
	tree.selectionMode = selectionMode
	tree.eventHandler = treeEventHandler
	tree.addItem = treeAddItem
	tree.draw = treeDraw

	return tree
end

----------------------------------------- FilesystemTree object -----------------------------------------

local function filesystemTreeUpdateFileListRecursively(tree, path, offset)
	local list = {}
	for file in fs.list(path) do
		table.insert(list, file)
	end

	local i, expandables = 1, {}
	while i <= #list do
		if fs.isDirectory(path .. list[i]) then
			table.insert(expandables, list[i])
			table.remove(list, i)
		else
			i = i + 1
		end
	end

	table.sort(expandables, function(a, b) return unicode.lower(a) < unicode.lower(b) end)
	table.sort(list, function(a, b) return unicode.lower(a) < unicode.lower(b) end)

	if tree.showMode == GUI.filesystemModes.both or tree.showMode == GUI.filesystemModes.directory then
		for i = 1, #expandables do
			tree:addItem(expandables[i], path .. expandables[i], offset, true)

			if tree.expandedItems[path .. expandables[i]] then
				filesystemTreeUpdateFileListRecursively(tree, path .. expandables[i], offset + 2)
			end
		end
	end

	if tree.showMode == GUI.filesystemModes.both or tree.showMode == GUI.filesystemModes.file then
		for i = 1, #list do
			tree:addItem(list[i], path .. list[i], offset, false,tree.extensionFilters and not tree.extensionFilters[fs.extension(path .. list[i], true)] or false)
		end
	end
end

local function filesystemTreeUpdateFileList(tree)
	tree.items = {}
	filesystemTreeUpdateFileListRecursively(tree, tree.workPath, 1)
end

local function filesystemTreeAddExtensionFilter(tree, extensionFilter)
	tree.extensionFilters = tree.extensionFilters or {}
	tree.extensionFilters[unicode.lower(extensionFilter)] = true
end

function GUI.filesystemTree(...)
	local tree = GUI.tree(...)

	tree.workPath = "/"
	tree.updateFileList = filesystemTreeUpdateFileList
	tree.addExtensionFilter = filesystemTreeAddExtensionFilter
	tree.onItemExpanded = function()
		tree:updateFileList()
	end

	return tree
end

----------------------------------------- Text Box object -----------------------------------------

local function textBoxCalculate(object)
	local doubleVerticalOffset = object.offset.vertical * 2
	object.textWidth = object.width - object.offset.horizontal * 2 - (object.scrollBarEnabled and 1 or 0)

	object.linesCopy = {}

	if object.autoWrap then
		for i = 1, #object.lines do
			local isTable = type(object.lines[i]) == "table"
			for subLine in (isTable and object.lines[i].text or object.lines[i]):gmatch("[^\n]+") do
				local wrappedLine = string.wrap(subLine, object.textWidth)
				for j = 1, #wrappedLine do
					table.insert(object.linesCopy, isTable and {text = wrappedLine[j], color = object.lines[i].color} or wrappedLine[j])
				end
			end
		end
	else
		for i = 1, #object.lines do
			table.insert(object.linesCopy, object.lines[i])
		end
	end

	if object.autoHeight then
		object.height = #object.linesCopy + doubleVerticalOffset
	end

	object.textHeight = object.height - doubleVerticalOffset
end

local function textBoxDraw(object)
	textBoxCalculate(object)

	if object.colors.background then
		buffer.square(object.x, object.y, object.width, object.height, object.colors.background, object.colors.text, " ", object.colors.transparency)
	end

	local x, y = nil, object.y + object.offset.vertical
	local lineType, text, textColor
	for i = object.currentLine, object.currentLine + object.textHeight - 1 do
		if object.linesCopy[i] then
			lineType = type(object.linesCopy[i])
			if lineType == "string" then
				text, textColor = string.limit(object.linesCopy[i], object.textWidth), object.colors.text
			elseif lineType == "table" then
				text, textColor = string.limit(object.linesCopy[i].text, object.textWidth), object.linesCopy[i].color
			else
				error("Unknown TextBox line type: " .. tostring(lineType))
			end

			x = GUI.getAlignmentCoordinates(
				{
					x = object.x + object.offset.horizontal,
					y = 1,
					width = object.textWidth,
					height = 1,
					alignment = object.alignment
				},
				{
					width = unicode.len(text),
					height = 1
				}
			)
			buffer.text(x, y, textColor, text)
			y = y + 1
		else
			break
		end
	end

	if object.scrollBarEnabled and object.textHeight < #object.lines then
		object.scrollBar.x = object.x + object.width - 1
		object.scrollBar.y = object.y
		object.scrollBar.height = object.height
		object.scrollBar.maximumValue = #object.lines - object.textHeight + 1
		object.scrollBar.value = object.currentLine
		object.scrollBar.shownValueCount = object.textHeight

		object.scrollBar:draw()
	end

	return object
end

local function scrollDownTextBox(object, count)
	count = math.min(count or 1, #object.lines - object.height - object.currentLine + object.offset.vertical * 2 + 1)
	if #object.lines >= object.height and object.currentLine < #object.lines - count then
		object.currentLine = object.currentLine + count
	end

	return object
end

local function scrollUpTextBox(object, count)
	count = count or 1
	if object.currentLine > count and object.currentLine >= 1 then object.currentLine = object.currentLine - count end
	return object
end

local function scrollToStartTextBox(object)
	object.currentLine = 1
	return object
end

local function scrollToEndTextBox(object)
	if #object.lines > object.textHeight then
		object.currentLine = #object.lines - object.textHeight + 1
	end

	return object
end

local function textBoxScrollEventHandler(mainContainer, object, eventData)
	if eventData[1] == "scroll" then
		if eventData[5] == 1 then
			object:scrollUp()
			mainContainer:draw()
			buffer.draw()
		else
			object:scrollDown()
			mainContainer:draw()
			buffer.draw()
		end
	end
end

function GUI.textBox(x, y, width, height, backgroundColor, textColor, lines, currentLine, horizontalOffset, verticalOffset, autoWrap, autoHeight)
	local object = GUI.object(x, y, width, height)
	
	object.eventHandler = textBoxScrollEventHandler
	object.colors = {
		text = textColor,
		background = backgroundColor
	}
	object.setAlignment = GUI.setAlignment
	object:setAlignment(GUI.alignment.horizontal.left, GUI.alignment.vertical.top)
	object.lines = lines
	object.currentLine = currentLine or 1
	object.draw = textBoxDraw
	object.scrollUp = scrollUpTextBox
	object.scrollDown = scrollDownTextBox
	object.scrollToStart = scrollToStartTextBox
	object.scrollToEnd = scrollToEndTextBox
	object.offset = {horizontal = horizontalOffset or 0, vertical = verticalOffset or 0}
	object.autoWrap = autoWrap
	object.autoHeight = autoHeight
	object.scrollBar = GUI.scrollBar(1, 1, 1, 1, 0xC3C3C3, 0x444444, 1, 1, 1, 1, 1, true)
	object.scrollBarEnabled = false

	textBoxCalculate(object)

	return object
end

----------------------------------------- Input object -----------------------------------------

local function inputSetCursorPosition(input, newPosition)
	if newPosition < 1 then
		newPosition = 1
	elseif newPosition > unicode.len(input.text) + 1 then
		newPosition = unicode.len(input.text) + 1
	end

	if newPosition > input.textCutFrom + input.width - 1 - input.textOffset * 2 then
		input.textCutFrom = input.textCutFrom + newPosition - (input.textCutFrom + input.width - 1 - input.textOffset * 2)
	elseif newPosition < input.textCutFrom then
		input.textCutFrom = newPosition
	end

	input.cursorPosition = newPosition

	return input
end

local function inputTextDrawMethod(x, y, color, text)
	buffer.text(x, y, color, text)
end

local function inputDraw(input)
	local background, foreground, transparency, text = input.colors.default.background, input.colors.default.text, input.colors.default.transparency, input.text

	if input.focused then
		background, foreground, transparency = input.colors.focused.background, input.colors.focused.text, input.colors.focused.transparency
	elseif input.text == "" then
		input.textCutFrom = 1
		text, foreground = input.placeholderText or "", input.colors.placeholderText
	end

	if background then
		buffer.square(input.x, input.y, input.width, input.height, background, foreground, " ", transparency)
	end

	local y = input.y + math.floor(input.height / 2)

	input.textDrawMethod(
		input.x + input.textOffset,
		y,
		foreground,
		unicode.sub(
			input.textMask and string.rep(input.textMask, unicode.len(text)) or text,
			input.textCutFrom,
			input.textCutFrom + input.width - 1 - input.textOffset * 2
		)
	)

	if input.cursorBlinkState then
		local index = buffer.getIndexByCoordinates(input.x + input.cursorPosition - input.textCutFrom + input.textOffset, y)
		local background = buffer.rawGet(index)
		buffer.rawSet(index, background, input.colors.cursor, input.cursorSymbol)
	end

	if input.autoCompleteEnabled then
		input.autoComplete.x = input.x
		if input.autoCompleteVerticalAlignment == GUI.alignment.vertical.top then
			input.autoComplete.y = input.y - input.autoComplete.height
		else
			input.autoComplete.y = input.y + input.height
		end
		input.autoComplete.width = input.width
		input.autoComplete:draw()
	end
end

local function inputEventHandler(mainContainer, input, mainEventData)
	if mainEventData[1] == "touch" then
		local textOnStart = input.text
		input.focused = true
		
		if input.historyEnabled then
			input.historyIndex = input.historyIndex + 1
		end

		if input.eraseTextOnFocus then
			input.text = ""
		end

		input.cursorBlinkState = true
		input:setCursorPosition(unicode.len(input.text) + 1)

		if input.autoCompleteEnabled then
			input.autoCompleteMatchMethod()
		end

		mainContainer:draw()
		buffer.draw()

		while true do
			local eventData = { event.pull(input.cursorBlinkDelay) }
			
			if eventData[1] == "touch" or eventData[1] == "drag" then
				if input:isClicked(eventData[3], eventData[4]) then
					input:setCursorPosition(input.textCutFrom + eventData[3] - input.x - input.textOffset)
					
					input.cursorBlinkState = true
					mainContainer:draw()
					buffer.draw()
				elseif input.autoComplete:isClicked(eventData[3], eventData[4]) then
					input.autoComplete.eventHandler(mainContainer, input.autoComplete, eventData)
				else
					input.cursorBlinkState = false
					break
				end
			elseif eventData[1] == "scroll" then
				input.autoComplete.eventHandler(mainContainer, input.autoComplete, eventData)
			elseif eventData[1] == "key_down" then
				-- Return
				if eventData[4] == 28 then
					if input.autoCompleteEnabled and input.autoComplete.itemCount > 0 then
						input.autoComplete.eventHandler(mainContainer, input.autoComplete, eventData)
					else
						if input.historyEnabled then
							-- Очистка истории
							for i = 1, (#input.history - input.historyLimit) do
								table.remove(input.history, 1)
							end

							-- Добавление введенных данных в историю
							if input.history[#input.history] ~= input.text and unicode.len(input.text) > 0 then
								table.insert(input.history, input.text)
							end
							input.historyIndex = #input.history
						end

						input.cursorBlinkState = false
						break
					end
				-- Arrows up/down/left/right
				elseif eventData[4] == 200 then
					if input.autoCompleteEnabled and input.autoComplete.selectedItem > 1 then
						input.autoComplete.eventHandler(mainContainer, input.autoComplete, eventData)
					else
						if input.historyEnabled and #input.history > 0 then
							-- Добавление уже введенного текста в историю при стрелке вверх
							if input.historyIndex == #input.history + 1 and unicode.len(input.text) > 0 then
								input.history[input.historyIndex] = input.text
							end

							input.historyIndex = input.historyIndex - 1
							if input.historyIndex > #input.history then
								input.historyIndex = #input.history
							elseif input.historyIndex < 1 then
								input.historyIndex = 1
							end

							input.text = input.history[input.historyIndex]
							input:setCursorPosition(unicode.len(input.text) + 1)

							if input.autoCompleteEnabled then
								input.autoCompleteMatchMethod()
							end
						end
					end
				elseif eventData[4] == 208 then
					if input.autoCompleteEnabled and input.historyIndex == #input.history + 1 then
						input.autoComplete.eventHandler(mainContainer, input.autoComplete, eventData)
					else
						if input.historyEnabled and #input.history > 0 then
							input.historyIndex = input.historyIndex + 1
							if input.historyIndex > #input.history then
								input.historyIndex = #input.history
							elseif input.historyIndex < 1 then
								input.historyIndex = 1
							end
							
							input.text = input.history[input.historyIndex]
							input:setCursorPosition(unicode.len(input.text) + 1)

							if input.autoCompleteEnabled then
								input.autoCompleteMatchMethod()
							end
						end
					end
				elseif eventData[4] == 203 then
					input:setCursorPosition(input.cursorPosition - 1)
				elseif eventData[4] == 205 then	
					input:setCursorPosition(input.cursorPosition + 1)
				-- Backspace
				elseif eventData[4] == 14 then
					input.text = unicode.sub(unicode.sub(input.text, 1, input.cursorPosition - 1), 1, -2) .. unicode.sub(input.text, input.cursorPosition, -1)
					input:setCursorPosition(input.cursorPosition - 1)
					
					if input.autoCompleteEnabled then
						input.autoCompleteMatchMethod()
					end
				-- Delete
				elseif eventData[4] == 211 then
					input.text = unicode.sub(input.text, 1, input.cursorPosition - 1) .. unicode.sub(input.text, input.cursorPosition + 1, -1)
					
					if input.autoCompleteEnabled then
						input.autoCompleteMatchMethod()
					end
				else
					local char = unicode.char(eventData[3])
					if not keyboard.isControl(eventData[3]) then
						input.text = unicode.sub(input.text, 1, input.cursorPosition - 1) .. char .. unicode.sub(input.text, input.cursorPosition, -1)
						input:setCursorPosition(input.cursorPosition + 1)

						if input.autoCompleteEnabled then
							input.autoCompleteMatchMethod()
						end
					end
				end

				input.cursorBlinkState = true
				mainContainer:draw()
				buffer.draw()
			elseif eventData[1] == "clipboard" then
				input.text = unicode.sub(input.text, 1, input.cursorPosition - 1) .. eventData[3] .. unicode.sub(input.text, input.cursorPosition, -1)
				input:setCursorPosition(input.cursorPosition + unicode.len(eventData[3]))
				
				input.cursorBlinkState = true
				mainContainer:draw()
				buffer.draw()
			elseif not eventData[1] then
				input.cursorBlinkState = not input.cursorBlinkState
				mainContainer:draw()
				buffer.draw()
			end
		end

		input.focused = false
		if input.autoCompleteEnabled then
			input.autoComplete:clear()
		end

		if input.validator then
			if not input.validator(input.text) then
				input.text = textOnStart
				input:setCursorPosition(unicode.len(input.text) + 1)
			end
		end
		
		GUI.callMethod(input.onInputFinished, mainContainer, input, mainEventData, input.text)
		
		mainContainer:draw()
		buffer.draw()
	end
end

function GUI.input(x, y, width, height, backgroundColor, textColor, placeholderTextColor, backgroundFocusedColor, textFocusedColor, text, placeholderText, eraseTextOnFocus, textMask)
	local input = GUI.object(x, y, width, height)
	
	input.colors = {
		default = {
			background = backgroundColor,
			text = textColor
		},
		focused = {
			background = backgroundFocusedColor,
			text = textFocusedColor
		},
		placeholderText = placeholderTextColor,
		cursor = 0x00A8FF
	}

	input.text = text or ""
	input.placeholderText = placeholderText
	input.eraseTextOnFocus = eraseTextOnFocus
	input.textMask = textMask

	input.textOffset = 1
	input.textCutFrom = 1
	input.cursorPosition = 1
	input.cursorSymbol = "┃"
	input.cursorBlinkDelay = 0.4
	input.cursorBlinkState = false
	input.textMask = textMask
	input.setCursorPosition = inputSetCursorPosition

	input.history = {}
	input.historyLimit = 20
	input.historyIndex = 0
	input.historyEnabled = false

	input.textDrawMethod = inputTextDrawMethod
	input.draw = inputDraw
	input.eventHandler = inputEventHandler

	input.autoComplete = GUI.autoComplete(1, 1, 30, 7, 0xE1E1E1, 0x999999, 0x3C3C3C, 0x3C3C3C, 0x999999, 0xE1E1E1, 0xC3C3C3, 0x444444)
	input.autoCompleteEnabled = false
	input.autoCompleteVerticalAlignment = GUI.alignment.vertical.bottom

	return input
end

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

local function autoCompleteDraw(object)
	local y, yEnd = object.y, object.y + object.height - 1

	buffer.square(object.x, object.y, object.width, object.height, object.colors.default.background, object.colors.default.text, " ")

	for i = object.fromItem, object.itemCount do
		local textColor, textMatchColor = object.colors.default.text, object.colors.default.textMatch
		if i == object.selectedItem then
			buffer.square(object.x, y, object.width, 1, object.colors.selected.background, object.colors.selected.text, " ")
			textColor, textMatchColor = object.colors.selected.text, object.colors.selected.textMatch
		end

		buffer.text(object.x + 1, y, textMatchColor, object.matchText)
		buffer.text(object.x + 1 + object.matchTextLength, y, textColor, unicode.sub(object.items[i], object.matchTextLength + 1, object.width - 2 - object.matchTextLength))

		y = y + 1
		if y > yEnd then
			break
		end
	end

	if object.itemCount > object.height then
		object.scrollBar.x = object.x + object.width - 1
		object.scrollBar.y = object.y
		object.scrollBar.height = object.height
		object.scrollBar.maximumValue = object.itemCount - object.height + 1
		object.scrollBar.value = object.fromItem
		object.scrollBar.shownValueCount = object.height

		object.scrollBar:draw()
	end
end

local function autoCompleteScroll(mainContainer, object, direction)
	if object.itemCount >= object.height then
		object.fromItem = object.fromItem + direction
		if object.fromItem < 1 then
			object.fromItem = 1
		elseif object.fromItem > object.itemCount - object.height + 1 then
			object.fromItem = object.itemCount - object.height + 1
		end
	end
end

local function autoCompleteEventHandler(mainContainer, object, eventData)
	if eventData[1] == "touch" then
		object.selectedItem = eventData[4] - object.y + object.fromItem
		mainContainer:draw()
		buffer.draw()

		GUI.callMethod(object.onItemSelected, mainContainer, object, eventData, object.selectedItem)
	elseif eventData[1] == "scroll" then
		autoCompleteScroll(mainContainer, object, -eventData[5])
		mainContainer:draw()
		buffer.draw()
	elseif eventData[1] == "key_down" then
		if eventData[4] == 28 then
			GUI.callMethod(object.onItemSelected, mainContainer, object, eventData, object.selectedItem)
		elseif eventData[4] == 200 then
			object.selectedItem = object.selectedItem - 1
			if object.selectedItem < 1 then
				object.selectedItem = 1
			end

			if object.selectedItem == object.fromItem - 1 then
				autoCompleteScroll(mainContainer, object, -1)
			end

			mainContainer:draw()
			buffer.draw()
		elseif eventData[4] == 208 then
			object.selectedItem = object.selectedItem + 1
			if object.selectedItem > object.itemCount then
				object.selectedItem = object.itemCount
			end

			if object.selectedItem == object.fromItem + object.height then
				autoCompleteScroll(mainContainer, object, 1)
			end
			
			mainContainer:draw()
			buffer.draw()
		end
	end
end

local function autoCompleteClear(object)
	object.items = {}
	object.itemCount = 0
	object.fromItem = 1
	object.selectedItem = 1
	object.height = 0
end

local function autoCompleteMatch(object, variants, text)
	object:clear()
	
	if text then
		for i = 1, #variants do
			if variants[i] ~= text and variants[i]:match("^" .. text) then
				table.insert(object.items, variants[i])
			end
		end
	else
		for i = 1, #variants do
			table.insert(object.items, variants[i])
		end
	end

	object.matchText = text or ""
	object.matchTextLength = unicode.len(object.matchText)

	table.sort(object.items, function(a, b) return unicode.lower(a) < unicode.lower(b) end)

	object.itemCount = #object.items
	object.height = math.min(object.itemCount, object.maximumHeight)

	return object
end

function GUI.autoComplete(x, y, width, maximumHeight, backgroundColor, textColor, textMatchColor, backgroundSelectedColor, textSelectedColor, textMatchSelectedColor, scrollBarBackground, scrollBarForeground)
	local object = GUI.object(x, y, width, maximumHeight)

	object.colors = {
		default = {
			background = backgroundColor,
			text = textColor,
			textMatch = textMatchColor	
		},
		selected = {
			background = backgroundSelectedColor,
			text = textSelectedColor,
			textMatch = textMatchSelectedColor
		}
	}

	object.maximumHeight = maximumHeight
	object.fromItem = 1
	object.selectedItem = 1
	object.items = {}
	object.matchText = " "
	object.matchTextLength = 1
	object.itemCount = 0

	object.scrollBar = GUI.scrollBar(1, 1, 1, 1, scrollBarBackground, scrollBarForeground, 1, 1, 1, 1, 1, true)

	object.match = autoCompleteMatch
	object.draw = autoCompleteDraw
	object.eventHandler = autoCompleteEventHandler
	object.clear = autoCompleteClear

	object:clear()

	return object
end

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

return GUI






