local keyboard = require("Keyboard")
local filesystem = require("Filesystem")
local event = require("Event")
local color = require("Color")
local image = require("Image")
local screen = require("Screen")
local paths = require("Paths")
local text = require("Text")
local number = require("Number")

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

local GUI = {
	ALIGNMENT_HORIZONTAL_LEFT = 1,
	ALIGNMENT_HORIZONTAL_CENTER = 2,
	ALIGNMENT_HORIZONTAL_RIGHT = 3,
	ALIGNMENT_VERTICAL_TOP = 4,
	ALIGNMENT_VERTICAL_CENTER = 5,
	ALIGNMENT_VERTICAL_BOTTOM = 6,

	DIRECTION_HORIZONTAL = 7,
	DIRECTION_VERTICAL = 8,

	SIZE_POLICY_ABSOLUTE = 9,
	SIZE_POLICY_RELATIVE = 10,

	IO_MODE_FILE = 11,
	IO_MODE_DIRECTORY = 12,
	IO_MODE_BOTH = 13,
	IO_MODE_OPEN = 14,
	IO_MODE_SAVE = 15,

	BUTTON_PRESS_DURATION = 0.2,
	BUTTON_ANIMATION_DURATION = 0.2,
	SWITCH_ANIMATION_DURATION = 0.3,
	FILESYSTEM_DIALOG_ANIMATION_DURATION = 0.5,
	
	CONTEXT_MENU_DEFAULT_BACKGROUND_COLOR = 0x1E1E1E,
	CONTEXT_MENU_DEFAULT_ICON_COLOR = 0x696969,
	CONTEXT_MENU_DEFAULT_TEXT_COLOR = 0xD2D2D2,
	CONTEXT_MENU_PRESSED_BACKGROUND_COLOR = 0x3366CC,
	CONTEXT_MENU_PRESSED_ICON_COLOR = 0xB4B4B4,
	CONTEXT_MENU_PRESSED_TEXT_COLOR = 0xFFFFFF,
	CONTEXT_MENU_DISABLED_ICON_COLOR = 0x5A5A5A,
	CONTEXT_MENU_DISABLED_TEXT_COLOR = 0x5A5A5A,
	CONTEXT_MENU_BACKGROUND_TRANSPARENCY = nil,
	CONTEXT_MENU_SHADOW_TRANSPARENCY = 0.4,
	CONTEXT_MENU_SEPARATOR_COLOR = 0x2D2D2D,

	BACKGROUND_CONTAINER_PANEL_COLOR = 0x0,
	BACKGROUND_CONTAINER_TITLE_COLOR = 0xE1E1E1,
	BACKGROUND_CONTAINER_PANEL_TRANSPARENCY = 0.3,

	WINDOW_BACKGROUND_PANEL_COLOR = 0xF0F0F0,
	WINDOW_SHADOW_TRANSPARENCY = 0.6,
	WINDOW_TITLE_BACKGROUND_COLOR = 0xE1E1E1,
	WINDOW_TITLE_TEXT_COLOR = 0x2D2D2D,
	WINDOW_TAB_BAR_DEFAULT_BACKGROUND_COLOR = 0x2D2D2D,
	WINDOW_TAB_BAR_DEFAULT_TEXT_COLOR = 0xF0F0F0,
	WINDOW_TAB_BAR_SELECTED_BACKGROUND_COLOR = 0xF0F0F0,
	WINDOW_TAB_BAR_SELECTED_TEXT_COLOR = 0x2D2D2D,

	LUA_SYNTAX_COLOR_SCHEME = {
		background = 0x1E1E1E,
		text = 0xE1E1E1,
		strings = 0x99FF80,
		loops = 0xFFFF98,
		comments = 0x898989,
		boolean = 0xFFDB40,
		logic = 0xFFCC66,
		numbers = 0x66DBFF,
		functions = 0xFFCC66,
		compares = 0xFFCC66,
		lineNumbersBackground = 0x2D2D2D,
		lineNumbersText = 0xC3C3C3,
		scrollBarBackground = 0x2D2D2D,
		scrollBarForeground = 0x5A5A5A,
		selection = 0x4B4B4B,
		indentation = 0x2D2D2D
	},

	LUA_SYNTAX_PATTERNS = {
		"[%.%,%>%<%=%~%+%-%*%/%^%#%%%&]", "compares", 0, 0,
		"[^%a%d][%.%d]+[^%a%d]", "numbers", 1, 1,
		"[^%a%d][%.%d]+$", "numbers", 1, 0,
		"0x%w+", "numbers", 0, 0,
		" not ", "logic", 0, 1,
		" or ", "logic", 0, 1,
		" and ", "logic", 0, 1,
		"function%(", "functions", 0, 1,
		"function%s[^%s%(%)%{%}%[%]]+%(", "functions", 9, 1,
		"nil", "boolean", 0, 0,
		"false", "boolean", 0, 0,
		"true", "boolean", 0, 0,
		" break$", "loops", 0, 0,
		"elseif ", "loops", 0, 1,
		"else[%s%;]", "loops", 0, 1,
		"else$", "loops", 0, 0,
		"function ", "loops", 0, 1,
		"local ", "loops", 0, 1,
		"return", "loops", 0, 0,
		"until ", "loops", 0, 1,
		"then", "loops", 0, 0,
		"if ", "loops", 0, 1,
		"repeat$", "loops", 0, 0,
		" in ", "loops", 0, 1,
		"for ", "loops", 0, 1,
		"end[%s%;]", "loops", 0, 1,
		"end$", "loops", 0, 0,
		"do ", "loops", 0, 1,
		"do$", "loops", 0, 0,
		"while ", "loops", 0, 1,
		"\'[^\']+\'", "strings", 0, 0,
		"\"[^\"]+\"", "strings", 0, 0,
		"%-%-.+", "comments", 0, 0,
	},
}

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

function GUI.setAlignment(object, horizontalAlignment, verticalAlignment)
	object.horizontalAlignment, object.verticalAlignment = horizontalAlignment, verticalAlignment
	
	return object
end

function GUI.getAlignmentCoordinates(x, y, width1, height1, horizontalAlignment, verticalAlignment, width2, height2)
	if horizontalAlignment == GUI.ALIGNMENT_HORIZONTAL_CENTER then
		x = x + width1 / 2 - width2 / 2
	elseif horizontalAlignment == GUI.ALIGNMENT_HORIZONTAL_RIGHT then
		x = x + width1 - width2
	elseif horizontalAlignment ~= GUI.ALIGNMENT_HORIZONTAL_LEFT then
		error("Unknown horizontal alignment: " .. tostring(horizontalAlignment))
	end

	if verticalAlignment == GUI.ALIGNMENT_VERTICAL_CENTER then
		y = y + height1 / 2 - height2 / 2
	elseif verticalAlignment == GUI.ALIGNMENT_VERTICAL_BOTTOM then
		y = y + height1 - height2
	elseif verticalAlignment ~= GUI.ALIGNMENT_VERTICAL_TOP then
		error("Unknown vertical alignment: " .. tostring(verticalAlignment))
	end

	return x, y
end

function GUI.getMarginCoordinates(x, y, horizontalAlignment, verticalAlignment, horizontalMargin, verticalMargin)
	if horizontalAlignment == GUI.ALIGNMENT_HORIZONTAL_RIGHT then
		x = x - horizontalMargin
	else
		x = x + horizontalMargin
	end
	
	if verticalAlignment == GUI.ALIGNMENT_VERTICAL_BOTTOM then
		y = y - verticalMargin
	else
		y = y + verticalMargin
	end

	return x, y
end

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

local function objectIsPointInside(object, x, y)
	return 
		x >= object.x and
		x < object.x + object.width and
		y >= object.y and
		y < object.y + object.height
end

local function objectDraw(object)
	return object
end

function GUI.object(x, y, width, height)
	return {
		x = x,
		y = y,
		width = width,
		height = height,
		isPointInside = objectIsPointInside,
		blockScreenEvents = true,
		draw = objectDraw
	}
end

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

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

	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 = containerObjectIndexOf(object)
	if objectIndex < #object.parent.children then
		object.parent.children[objectIndex], object.parent.children[objectIndex + 1] = object.parent.children[objectIndex + 1], object.parent.children[objectIndex]
	end
	
	return object
end

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

local function containerObjectMoveToFront(object)
	table.remove(object.parent.children, containerObjectIndexOf(object))
	table.insert(object.parent.children, object)
	
	return object
end

local function containerObjectMoveToBack(object)
	table.remove(object.parent.children, containerObjectIndexOf(object))
	table.insert(object.parent.children, 1, object)
	
	return object
end

local function containerObjectRemove(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("GUI", "animationStarted")
end

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

local function containerObjectAnimationRemove(animation)
	animation.removeLater = true
end

local function containerObjectAddAnimation(object, frameHandler, onFinish)
	local animation = {
		object = object,
		position = 0,
		start = containerObjectAnimationStart,
		stop = containerObjectAnimationStop,
		remove = containerObjectAnimationRemove,
		frameHandler = frameHandler,
		onFinish = onFinish,
	}

	object.workspace.animations = object.workspace.animations or {}
	table.insert(object.workspace.animations, animation)

	return animation
end

local function containerAddChild(container, object, atIndex)
	-- Parent containers
	object.parent = container

	local function updateWorkspace(object, workspace)
		object.workspace = workspace

		if object.children then
			for i = 1, #object.children do
				updateWorkspace(object.children[i], workspace)
			end
		end
	end

	updateWorkspace(object, container.workspace or container)

	-- Position
	object.localX = object.x
	object.localY = object.y

	-- Additional methods after adding to parent container
	object.indexOf = containerObjectIndexOf
	object.moveToFront = containerObjectMoveToFront
	object.moveToBack = containerObjectMoveToBack
	object.moveForward = containerObjectMoveForward
	object.moveBackward = containerObjectMoveBackward
	object.remove = containerObjectRemove
	object.addAnimation = containerObjectAddAnimation

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

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

local function getRectangleBounds(R1X1, R1Y1, R1X2, R1Y2, R2X1, R2Y1, R2X2, R2Y2)
	if R2X1 <= R1X2 and R2Y1 <= R1Y2 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

local function containerDraw(container)
	local R1X1, R1Y1, R1X2, R1Y2, child = screen.getDrawLimit()

	local boundsX1, boundsY1, boundsX2, boundsY2 = getRectangleBounds(
		R1X1,
		R1Y1,
		R1X2,
		R1Y2,
		container.x,
		container.y,
		container.x + container.width - 1,
		container.y + container.height - 1
	)

	if boundsX1 then
		screen.setDrawLimit(boundsX1, boundsY1, boundsX2, boundsY2)
		
		for i = 1, #container.children do
			child = container.children[i]
			
			if not child.hidden then
				child.x, child.y = container.x + child.localX - 1, container.y + child.localY - 1
				child:draw()
			end
		end

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

	-- if container.workspace and container.workspace.capturedObject == container then
	-- 	screen.drawRectangle(container.x, container.y, container.width, 1, 0xFF0000, 0x0, " ")
	-- end

	return container
end

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

	container.children = {}
	container.blockScreenEvents = false
	
	container.draw = containerDraw
	container.removeChildren = containerRemoveChildren
	container.addChild = containerAddChild
	
	return container
end

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

local function workspaceStart(workspace, eventPullTimeout)
	local
		e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12, e13, e14, e15, e16, e17, e18, e19, e20, e21, e22, e23, e24, e25, e26, e27, e28, e29, e30, e31, e32,
		isScreenEvent,
		roundedX,
		roundedY,
		capturedObject,
		animation,
		animationIndex,
		animationOnFinishMethodsIndex,
		animationOnFinishMethods

	local function processObject(object, boundsX1, boundsY1, boundsX2, boundsY2)
		local govno = false

		if isScreenEvent then
			if workspace.capturedObject then
				if workspace.capturedObject == object then
					govno = object.blockScreenEvents

				elseif object.ignoresCapturedObject then
					if not boundsX1 or not (
						roundedX >= boundsX1
						and roundedX <= boundsX2
						and roundedY >= boundsY1
						and roundedY <= boundsY2
					) then
						goto skipEventHandling
					end
				else
					goto skipEventHandling
				end
			else
				if
					not boundsX1 or not (
						roundedX >= boundsX1
						and roundedX <= boundsX2
						and roundedY >= boundsY1
						and roundedY <= boundsY2
					)
				then
					goto skipEventHandling
				else
					govno = object.blockScreenEvents
				end
			end

			if object.eventHandler and not object.disabled then
				object.eventHandler(workspace, object, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12, e13, e14, e15, e16, e17, e18, e19, e20, e21, e22, e23, e24, e25, e26, e27, e28, e29, e30, e31, e32)
			end
		else
			if
				not workspace.capturedObject
				or workspace.capturedObject == object
				or object.ignoresCapturedObject
			then
				if object.eventHandler and not object.disabled then
					object.eventHandler(workspace, object, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12, e13, e14, e15, e16, e17, e18, e19, e20, e21, e22, e23, e24, e25, e26, e27, e28, e29, e30, e31, e32)
				end
			end
		end
		

		-- Container
		::skipEventHandling::

		if object.children and boundsX1 then
			local child, newBoundsX1, newBoundsY1, newBoundsX2, newBoundsY2

			for i = #object.children, 1, -1 do
				child = object.children[i]

				if not child.hidden then
					newBoundsX1, newBoundsY1, newBoundsX2, newBoundsY2 = getRectangleBounds(
						boundsX1,
						boundsY1,
						boundsX2,
						boundsY2,

						child.x,
						child.y,
						child.x + child.width - 1,
						child.y + child.height - 1
					)

					if processObject(
						child,
						newBoundsX1,
						newBoundsY1,
						newBoundsX2,
						newBoundsY2
					) then
						return true
					end
				end
			end
		end

		if govno then
			return true
		end
	end

	workspace.eventPullTimeout = eventPullTimeout

	repeat
		e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12, e13, e14, e15, e16, e17, e18, e19, e20, e21, e22, e23, e24, e25, e26, e27, e28, e29, e30, e31, e32 = event.pull(workspace.animations and 0 or workspace.eventPullTimeout)
		
		isScreenEvent =
			e1 == "touch" or
			e1 == "drag" or
			e1 == "drop" or
			e1 == "scroll" or
			e1 == "double_touch"

		if isScreenEvent then
			roundedX, roundedY = math.ceil(e3), math.ceil(e4)
		end

		processObject(
			workspace,
			workspace.x,
			workspace.y,
			workspace.x + workspace.width - 1,
			workspace.y + workspace.height - 1
		)

		if workspace.animations then
			animationIndex, animationOnFinishMethodsIndex, animationOnFinishMethods = 1, 1, {}
			
			-- Продрачиваем анимации и вызываем обработчики кадров
			while animationIndex <= #workspace.animations do
				animation = workspace.animations[animationIndex]

				if animation.removeLater then
					table.remove(workspace.animations, animationIndex)

					if #workspace.animations == 0 then
						workspace.animations = nil
						
						break
					end
				else
					if animation.started then
						animation.position = (computer.uptime() - animation.startUptime) / animation.duration
						
						if animation.position < 1 then
							animation.frameHandler(animation)
						else
							animation.position, animation.started = 1, false
							animation.frameHandler(animation)
							
							if animation.onFinish then
								animationOnFinishMethods[animationOnFinishMethodsIndex] = animation
								animationOnFinishMethodsIndex = animationOnFinishMethodsIndex + 1
							end
						end
					end

					animationIndex = animationIndex + 1
				end
			end

			-- По завершению продрочки отрисовываем изменения на экране
			workspace:draw()

			-- Вызываем поочередно все методы .onFinish
			for i = 1, #animationOnFinishMethods do
				animationOnFinishMethods[i].onFinish(animationOnFinishMethods[i])
			end
		end
	until workspace.needClose

	workspace.needClose = nil
end

local function workspaceStop(workspace)
	workspace.needClose = true
end

local function workspaceDraw(object, ...)
	containerDraw(object)

	screen.update(...)
end

function GUI.workspace(x, y, width, height)
	local workspace = GUI.container(x or 1, y or 1, width or screen.getWidth(), height or screen.getHeight())
	
	workspace.draw = workspaceDraw
	workspace.start = workspaceStart
	workspace.stop = workspaceStop

	return workspace
end

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

local function pressableDraw(pressable)
	local background = pressable.pressed and pressable.colors.pressed.background or pressable.disabled and pressable.colors.disabled.background or pressable.colors.default.background
	local text = pressable.pressed and pressable.colors.pressed.text or pressable.disabled and pressable.colors.disabled.text or pressable.colors.default.text

	if background then
		screen.drawRectangle(pressable.x, pressable.y, pressable.width, pressable.height, background, text, " ")
	end
	
	screen.drawText(math.floor(pressable.x + pressable.width / 2 - unicode.wlen(pressable.text) / 2), math.floor(pressable.y + pressable.height / 2), text, pressable.text)
end

local function pressableHandlePress(workspace, pressable, ...)
	pressable.pressed = not pressable.pressed
	workspace:draw()

	if not pressable.switchMode then
		pressable.pressed = not pressable.pressed
		event.sleep(GUI.BUTTON_PRESS_DURATION)
		
		workspace:draw()
	end

	if pressable.onTouch then
		pressable.onTouch(workspace, pressable, ...)
	end
end

local function pressableEventHandler(workspace, pressable, e1, ...)
	if e1 == "touch" then
		pressableHandlePress(workspace, pressable, e1, ...)
	end
end

local function pressable(x, y, width, height, backgroundColor, textColor, backgroundPressedColor, textPressedColor, backgroundDisabledColor, textDisabledColor, text)
	local pressable = GUI.object(x, y, width, height)

	pressable.colors = {
		default = {
			background = backgroundColor,
			text = textColor
		},
		pressed = {
			background = backgroundPressedColor,
			text = textPressedColor
		},
		disabled = {
			background = backgroundDisabledColor,
			text = textDisabledColor
		}
	}

	pressable.pressed = false
	pressable.text = text
	pressable.draw = pressableDraw
	pressable.eventHandler = pressableEventHandler

	return pressable
end

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

local function buttonPlayAnimation(button, onFinish)
	button.animationStarted = true
	button:addAnimation(
		function(animation)
			if button.pressed then
				if button.colors.default.background and button.colors.pressed.background then
					button.animationCurrentBackground = color.transition(button.colors.pressed.background, button.colors.default.background, animation.position)
				end
				
				button.animationCurrentText = color.transition(button.colors.pressed.text, button.colors.default.text, animation.position)
			else
				if button.colors.default.background and button.colors.pressed.background then
					button.animationCurrentBackground = color.transition(button.colors.default.background, button.colors.pressed.background, animation.position)
				end

				button.animationCurrentText = color.transition(button.colors.default.text, button.colors.pressed.text, animation.position)
			end
		end,
		function(animation)
			button.animationStarted = false
			button.pressed = not button.pressed
			onFinish(animation)
		end
	):start(button.animationDuration)
end

local function buttonPress(button, workspace, object, ...)
	if button.animated then
		local eventData = {...}
		
		buttonPlayAnimation(button, function(animation)
			if button.onTouch then
				button.onTouch(workspace, button, table.unpack(eventData))
			end

			animation:remove()

			if not button.switchMode then
				buttonPlayAnimation(button, function(animation)
					animation:remove()
				end)
			end
		end)
	else
		pressableHandlePress(workspace, button, ...)
	end
end

local function buttonEventHandler(workspace, button, e1, ...)
	if e1 == "touch" and (not button.animated or not button.animationStarted) then
		button:press(workspace, button, e1, ...)
	end
end

local function buttonGetColors(button)
	if button.disabled then
		return button.colors.disabled.background, button.colors.disabled.text
	else
		if button.animated and button.animationStarted then
			return button.animationCurrentBackground, button.animationCurrentText
		else
			if button.pressed then
				return button.colors.pressed.background, button.colors.pressed.text
			else
				return button.colors.default.background, button.colors.default.text
			end
		end
	end
end

local function buttonDrawText(button, textColor)
	screen.drawText(math.floor(button.x + button.width / 2 - unicode.wlen(button.text) / 2), math.floor(button.y + button.height / 2), textColor, button.text)
end

local function buttonDraw(button)
	local backgroundColor, textColor = buttonGetColors(button)
	
	if backgroundColor then
		screen.drawRectangle(button.x, button.y, button.width, button.height, backgroundColor, textColor, " ", button.colors.transparency)
	end
	buttonDrawText(button, textColor)
end

local function framedButtonDraw(button)
	local backgroundColor, textColor = buttonGetColors(button)
	
	if backgroundColor then
		screen.drawFrame(button.x, button.y, button.width, button.height, backgroundColor)
	end
	buttonDrawText(button, textColor)
end

local function roundedButtonDraw(button)
	local backgroundColor, textColor = buttonGetColors(button)

	if backgroundColor then
		local x2, y2 = button.x + button.width - 1, button.y + button.height - 1
		
		if button.height > 1 then
			screen.drawText(button.x + 1, button.y, backgroundColor, string.rep("▄", button.width - 2))
			screen.drawText(button.x, button.y, backgroundColor, "⣠")
			screen.drawText(x2, button.y, backgroundColor, "⣄")
			
			screen.drawRectangle(button.x, button.y + 1, button.width, button.height - 2, backgroundColor, textColor, " ")
			
			screen.drawText(button.x + 1, y2, backgroundColor, string.rep("▀", button.width - 2))
			screen.drawText(button.x, y2, backgroundColor, "⠙")
			screen.drawText(x2, y2, backgroundColor, "⠋")
		else
			screen.drawRectangle(button.x + 1, button.y, button.width - 2, button.height, backgroundColor, textColor, " ")
			GUI.drawRoundedCorners(button.x, button.y, button.width, button.height, backgroundColor)
		end
	end

	buttonDrawText(button, textColor)
end

local function tagButtonDraw(button)
	local backgroundColor, textColor = buttonGetColors(button)
	
	screen.drawRectangle(button.x, button.y, button.width, button.height, backgroundColor, textColor, " ")
	screen.drawText(button.x - 1, button.y, backgroundColor, "◀")
	buttonDrawText(button, textColor)
end

local function buttonCreate(x, y, width, height, backgroundColor, textColor, backgroundPressedColor, textPressedColor, text)
	local button = pressable(x, y, width, height, backgroundColor, textColor, backgroundPressedColor, textPressedColor, 0x878787, 0xA5A5A5, text)

	button.animationDuration = GUI.BUTTON_ANIMATION_DURATION
	button.animated = true

	button.animationCurrentBackground = backgroundColor
	button.animationCurrentText = textColor
	
	button.press = buttonPress
	button.eventHandler = buttonEventHandler

	return button
end

local function adaptiveButtonCreate(x, y, xOffset, yOffset, backgroundColor, textColor, backgroundPressedColor, textPressedColor, text)
	return buttonCreate(x, y, unicode.wlen(text) + xOffset * 2, yOffset * 2 + 1, backgroundColor, textColor, backgroundPressedColor, textPressedColor, text)
end

function GUI.button(...)
	local button = buttonCreate(...)
	button.draw = buttonDraw

	return button
end

function GUI.adaptiveButton(...)
	local button = adaptiveButtonCreate(...)
	button.draw = buttonDraw

	return button
end

function GUI.framedButton(...)
	local button = buttonCreate(...)
	button.draw = framedButtonDraw

	return button
end

function GUI.adaptiveFramedButton(...)
	local button = adaptiveButtonCreate(...)
	button.draw = framedButtonDraw

	return button
end

function GUI.roundedButton(...)
	local button = buttonCreate(...)
	button.draw = roundedButtonDraw

	return button
end

function GUI.adaptiveRoundedButton(...)
	local button = adaptiveButtonCreate(...)
	button.draw = roundedButtonDraw

	return button
end

function GUI.tagButton(...)
	local button = buttonCreate(...)
	button.draw = tagButtonDraw

	return button
end

function GUI.adaptiveTagButton(...)
	local button = adaptiveButtonCreate(...)
	button.draw = tagButtonDraw

	return button
end

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

local function drawPanel(object)
	screen.drawRectangle(object.x, object.y, object.width, object.height, object.colors.background, 0x0, " ", 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

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

local function blurredPanelDraw(object)
	screen.blur(object.x, object.y, object.width, object.height, object.radius, object.color, object.transparency)
end

function GUI.blurredPanel(x, y, width, height, radius, color, transparency)
	local object = GUI.object(x, y, width, height)

	object.radius = radius or 3
	object.color = color
	object.transparency = transparency

	object.draw = blurredPanelDraw

	return object
end

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

local function drawLabel(object)
	local xText, yText = GUI.getAlignmentCoordinates(
		object.x,
		object.y,
		object.width,
		object.height,
		object.horizontalAlignment,
		object.verticalAlignment,
		unicode.wlen(object.text),
		1
	)
	screen.drawText(math.floor(xText), math.floor(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

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

local function drawImage(object)
	screen.drawImage(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

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

function GUI.actionButtons(x, y, fatSymbol)
	local symbol = "●"
	
	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

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

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

	if object.showValue then
		local stringValue = (object.valuePrefix or "") .. object.value .. (object.valuePostfix or "")
		screen.drawText(math.floor(object.x + object.width / 2 - unicode.wlen(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

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

function GUI.drawShadow(x, y, width, height, transparency, thin)
	if thin then
		screen.drawRectangle(x + width, y + 1, 1, height - 1, 0x0, 0x0, " ", transparency)
		screen.drawText(x + 1, y + height, 0x0, string.rep("▀", width), transparency)
		screen.drawText(x + width, y, 0x0, "▄", transparency)
	else
		screen.drawRectangle(x + width, y + 1, 2, height, 0x0, 0x0, " ", transparency)
		screen.drawRectangle(x + 2, y + height, width - 2, 1, 0x0, 0x0, " ", transparency)
	end
end

function GUI.drawRoundedCorners(x, y, width, height, color, transparency)
	screen.drawText(x, y, color, "◖", transparency)
	screen.drawText(x + width - 1, y, color, "◗", transparency)
end

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

function GUI.alert(...)
	local args = {...}
	
	for i = 1, #args do
		if type(args[i]) == "table" then
			args[i] = text.serialize(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 bufferWidth, bufferHeight = screen.getResolution()
	local width = math.floor(bufferWidth * 0.5)
	local textWidth = width - image.getWidth(sign) - 2

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

	local workspace = GUI.workspace(1, math.floor(bufferHeight / 2 - height / 2), bufferWidth, height + offset * 2)
	local oldPixels = screen.copy(workspace.x, workspace.y, workspace.width, workspace.height)

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

	workspace.eventHandler = function(workspace, object, e1, e2, e3, e4, ...)
		if e1 == "key_down" and e4 == 28 then
			button.animated = false
			button:press(workspace, object, e1, e2, e3, e4, ...)
		end
	end

	workspace:draw(true)
	workspace:start()
end

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

local function codeViewDraw(codeView)
	local y, toLine, colorScheme, patterns = codeView.y, codeView.fromLine + codeView.height - 1, codeView.syntaxColorScheme, codeView.syntaxPatterns
	
	-- Line numbers bar and code area
	codeView.lineNumbersWidth = unicode.wlen(tostring(toLine)) + 2
	codeView.codeAreaPosition = codeView.x + codeView.lineNumbersWidth
	codeView.codeAreaWidth = codeView.width - codeView.lineNumbersWidth
	
	-- Line numbers 
	screen.drawRectangle(codeView.x, y, codeView.lineNumbersWidth, codeView.height, colorScheme.lineNumbersBackground, colorScheme.lineNumbersText, " ")	
	
	-- Background
	screen.drawRectangle(codeView.codeAreaPosition, y, codeView.codeAreaWidth, codeView.height, colorScheme.background, colorScheme.text, " ")
	
	-- Line numbers texts
	local text
	for line = codeView.fromLine, toLine do
		if codeView.lines[line] then
			text = line .. ""
			if codeView.highlights[line] then
				screen.drawRectangle(codeView.x, y, codeView.lineNumbersWidth, 1, codeView.highlights[line], colorScheme.text, " ", 0.3)
				screen.drawRectangle(codeView.codeAreaPosition, y, codeView.codeAreaWidth, 1, codeView.highlights[line], colorScheme.text, " ")
			end

			screen.drawText(codeView.codeAreaPosition - unicode.wlen(text) - 1, y, colorScheme.lineNumbersText, text)
			
			y = y + 1
		else
			break
		end	
	end
	
	if #codeView.selections > 0 then
		local function drawUpperSelection(y, selectionIndex)
			screen.drawRectangle(
				math.max(codeView.codeAreaPosition, 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 colorScheme.selection,
				colorScheme.text,
				" "
			)
		end

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

		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
				screen.drawRectangle(
					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 colorScheme.selection, 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
					screen.drawRectangle(
						codeView.codeAreaPosition, 
						y + codeView.selections[selectionIndex].from.line - codeView.fromLine,
						codeView.codeAreaWidth,
						1,
						codeView.selections[selectionIndex].color or colorScheme.selection,
						colorScheme.text,
						" "
					)

					y = y + 1
				end

				drawLowerSelection(y, selectionIndex)
			end
		end
	end

	-- Code strings
	y = codeView.y
	for i = codeView.fromLine, toLine do
		if codeView.lines[i] then
			if codeView.syntaxHighlight then
				GUI.highlightString(
					codeView.codeAreaPosition + 1,
					y,
					codeView.fromSymbol,
					codeView.indentationWidth,
					patterns,
					colorScheme,
					codeView.lines[i]
				)
			else
				screen.drawText(
					codeView.codeAreaPosition + 1,
					y,
					colorScheme.text,
					unicode.sub(
						codeView.lines[i],
						codeView.fromSymbol,
						codeView.fromSymbol + codeView.codeAreaWidth - 3
					)
				)
			end

			y = y + 1
		else
			break
		end
	end

	-- Scrollbars
	if #codeView.lines > codeView.height then
		codeView.verticalScrollBar.colors.background, codeView.verticalScrollBar.colors.foreground = colorScheme.scrollBarBackground, colorScheme.scrollBarForeground
		codeView.verticalScrollBar.minimumValue, codeView.verticalScrollBar.maximumValue, codeView.verticalScrollBar.value, codeView.verticalScrollBar.shownValueCount = 1, #codeView.lines, codeView.fromLine, codeView.height
		codeView.verticalScrollBar.localX = codeView.width
		codeView.verticalScrollBar.localY = 1
		codeView.verticalScrollBar.height = codeView.height - 1
		codeView.verticalScrollBar.hidden = false
	else
		codeView.verticalScrollBar.hidden = true
	end

	if codeView.maximumLineLength > codeView.codeAreaWidth - 2 then
		codeView.horizontalScrollBar.colors.background, codeView.horizontalScrollBar.colors.foreground = colorScheme.scrollBarBackground, colorScheme.scrollBarForeground
		codeView.horizontalScrollBar.minimumValue, codeView.horizontalScrollBar.maximumValue, codeView.horizontalScrollBar.value, codeView.horizontalScrollBar.shownValueCount = 1, codeView.maximumLineLength, codeView.fromSymbol, codeView.codeAreaWidth - 2
		codeView.horizontalScrollBar.localX = codeView.lineNumbersWidth + 1
		codeView.horizontalScrollBar.localY = codeView.height
		codeView.horizontalScrollBar.width = codeView.codeAreaWidth - 1
		codeView.horizontalScrollBar.hidden = false
	else
		codeView.horizontalScrollBar.hidden = true
	end

	codeView:overrideDraw()
end

function GUI.codeView(x, y, width, height, fromSymbol, fromLine, maximumLineLength, selections, highlights, syntaxPatterns, syntaxColorScheme, syntaxHighlight, lines)	
	local codeView = GUI.container(x, y, width, height)
	
	codeView.blockScreenEvents = true
	codeView.lines = lines
	codeView.fromSymbol = fromSymbol
	codeView.fromLine = fromLine
	codeView.maximumLineLength = maximumLineLength
	codeView.selections = selections or {}
	codeView.highlights = highlights or {}
	codeView.syntaxHighlight = syntaxHighlight
	codeView.syntaxPatterns = syntaxPatterns
	codeView.syntaxColorScheme = syntaxColorScheme
	codeView.indentationWidth = 2

	codeView.verticalScrollBar = codeView:addChild(GUI.scrollBar(1, 1, 1, 1, 0x0, 0x0, 1, 1, 1, 1, 1, true))
	codeView.horizontalScrollBar = codeView:addChild(GUI.scrollBar(1, 1, 1, 1, 0x0, 0x0, 1, 1, 1, 1, 1, true))

	codeView.overrideDraw = codeView.draw
	codeView.draw = codeViewDraw

	return codeView
end 

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

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

local function colorSelectorEventHandler(workspace, object, e1, ...)
	if e1 == "touch" then
		local eventData = {...}
		object.pressed = true

		local palette = workspace:addChild(GUI.palette(1, 1, object.color))
		palette.localX, palette.localY = math.floor(workspace.width / 2 - palette.width / 2), math.floor(workspace.height / 2 - palette.height / 2)

		palette.cancelButton.onTouch = function()
			object.pressed = false
			palette:remove()
			workspace:draw()

			if object.onColorSelected then
				object.onColorSelected(workspace, object, e1, table.unpack(eventData))
			end
		end

		palette.submitButton.onTouch = function()
			object.color = palette.color.integer
			palette.cancelButton.onTouch()
		end
		
		workspace:draw()
	end
end

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

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

local function getAxisValue(num, postfix, roundValues)
	if roundValues then
		return math.floor(num) .. postfix
	else
		local integer, fractional = math.modf(num)
		local firstPart, secondPart = "", ""
		if math.abs(integer) >= 1000 then
			return number.shorten(integer, 2) .. postfix
		else
			if math.abs(fractional) > 0 then
				return string.format("%.2f", num) .. postfix
			else
				return num .. 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.wlen(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.wlen(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
			screen.drawText(chartX - unicode.wlen(yAxisValues[i].value) - 2, yAxisValues[i].y, object.colors.axisValue, yAxisValues[i].value)
		end
		screen.drawText(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)
			screen.drawText(math.floor(x - unicode.wlen(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)
		screen.drawText(object.x + object.width - unicode.wlen(value), object.y + object.height - 1, object.colors.axisValue, value)
	end

	-- Axis lines
	for y = object.y, object.y + chartHeight - 1 do
		screen.drawText(chartX - 1, y, object.colors.axis, "┨")
	end
	screen.drawText(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)
				screen.drawSemiPixelRectangle(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)
				screen.drawSemiPixelRectangle(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
			screen.drawSemiPixelLine(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

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

local function switchAndLabelDraw(switchAndLabel)
	switchAndLabel.label.width = switchAndLabel.width
	switchAndLabel.switch.localX = switchAndLabel.width - switchAndLabel.switch.width + 1

	switchAndLabel.label.x, switchAndLabel.label.y = switchAndLabel.x + switchAndLabel.label.localX - 1, switchAndLabel.y + switchAndLabel.label.localY - 1
	switchAndLabel.switch.x, switchAndLabel.switch.y = switchAndLabel.x + switchAndLabel.switch.localX - 1, switchAndLabel.y + switchAndLabel.switch.localY - 1
	
	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

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

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 number.roundToDecimalPlaces(object.maximumValue, 2)), tostring(object.roundValues and math.floor(object.minimumValue) or number.roundToDecimalPlaces(object.minimumValue, 2))
		screen.drawText(object.x - unicode.wlen(stringMinimumValue) - 1, object.y, object.colors.value, stringMinimumValue)
		screen.drawText(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 number.roundToDecimalPlaces(object.value, 2)) .. (object.currentValuePostfix or "")
		screen.drawText(math.floor(object.x + object.width / 2 - unicode.wlen(stringCurrentValue) / 2), object.y + 1, object.colors.value, stringCurrentValue)
	end

	local activeWidth = number.round((object.value - object.minimumValue) / (object.maximumValue - object.minimumValue) * object.width)
	screen.drawText(object.x, object.y, object.colors.passive, string.rep("━", object.width))
	screen.drawText(object.x, object.y, object.colors.active, string.rep("━", activeWidth))
	screen.drawText(activeWidth >= object.width and object.x + activeWidth - 1 or object.x + activeWidth, object.y, object.colors.pipe, "⬤")

	return object
end

local function sliderEventHandler(workspace, object, e1, e2, e3, e4, e5, ...)
	if e1 == "touch" or e1 == "drag" then
		local clickPosition = e3 - object.x

		if clickPosition <= 0 then
			object.value = object.minimumValue
		elseif clickPosition >= object.width - 1 then
			object.value = object.maximumValue
		else
			object.value = object.minimumValue + (clickPosition / object.width * (object.maximumValue - object.minimumValue))
		end

		workspace:draw()

		if object.onValueChanged then
			object.onValueChanged(workspace, object, e1, e2, e3, e4, e5, ...)
		end

	-- Sucks on scrollable views

	-- elseif e1 == "scroll" then
	-- 	object.value = object.value + (object.maximumValue - object.minimumValue) * object.scrollSensivity * e5

	-- 	if object.value > object.maximumValue then
	-- 		object.value = object.maximumValue 
	-- 	elseif object.value < object.minimumValue then
	-- 		object.value = object.minimumValue
	-- 	end

	-- 	workspace:draw()

	-- 	if object.onValueChanged then
	-- 		object.onValueChanged(workspace, object, e1, e2, e3, e4, e5, ...)
	-- 	end
	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.scrollSensivity = 0.05
	object.showMaximumAndMinimumValues = showMaximumAndMinimumValues
	object.currentValuePrefix = currentValuePrefix
	object.currentValuePostfix = currentValuePostfix
	object.roundValues = false
	
	return object
end

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

local function switchDraw(switch)
	-- ◖◗
	-- ⠆⠰

	-- Sides
	if switch.pipePosition > 1 then
		screen.drawText(switch.x, switch.y, switch.colors.active, "◖")
	end

	if switch.pipePosition < switch.width - 1 then
		screen.drawText(switch.x + switch.width - 1, switch.y, switch.colors.passive, "◗")
	end

	-- Background
	if switch.width > 2 then
		-- Active
		local width = switch.pipePosition - 1

		if width > 0 then
			screen.drawRectangle(switch.x + 1, switch.y, width, 1, switch.colors.active, 0x0, " ")
		end

		-- Passive
		width = switch.width - switch.pipePosition - 1

		if width > 0 then
			screen.drawRectangle(switch.x + switch.pipePosition, switch.y, width, 1, switch.colors.passive, 0x0, " ")
		end
	end

	-- Pipe
	screen.drawText(switch.x + switch.pipePosition - 1, switch.y, switch.colors.pipe, "◖")
	screen.set(switch.x + switch.pipePosition, switch.y, switch.colors.pipe, switch.colors.pipe, " ")
	screen.drawText(switch.x + switch.pipePosition + 1, switch.y, switch.colors.pipe, "◗")
	
	return switch
end

local function switchSetState(switch, state)
	switch.state = state
	switch.pipePosition = switch.state and switch.width - 2 or 1

	return switch
end

local function switchEventHandler(workspace, switch, e1, ...)
	if e1 == "touch" then
		local eventData = {...}

		switch.state = not switch.state
		
		switch:addAnimation(
			function(animation)
				if switch.state then
					switch.pipePosition = number.round(1 + animation.position * (switch.width - 3))
				else	
					switch.pipePosition = number.round(1 + (1 - animation.position) * (switch.width - 3))
				end
			end,
			
			function(animation)
				animation:remove()
				if switch.onStateChanged then
					switch.onStateChanged(switch, e1, table.unpack(eventData))
				end
			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.animated = true
	switch.animationDuration = GUI.SWITCH_ANIMATION_DURATION
	switch.setState = switchSetState

	switch:setState(state)
	
	return switch
end

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

local function layoutCheckCell(layout, column, row)
	if column < 1 or column > #layout.columnSizes or row < 1 or row > #layout.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.SIZE_POLICY_ABSOLUTE then
			absoluteTotalSize = absoluteTotalSize + array[i].size
		end
	end
	return absoluteTotalSize
end

local function layoutGetCalculatedSize(array, index, dependency)
	if array[index].sizePolicy == GUI.SIZE_POLICY_RELATIVE 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.columnSizes), layout.height - layoutGetAbsoluteTotalSize(layout.rowSizes)
	for row = 1, #layout.rowSizes do
		layoutGetCalculatedSize(layout.rowSizes, row, rowPercentageTotalSize)
		for column = 1, #layout.columnSizes do
			layoutGetCalculatedSize(layout.columnSizes, column, columnPercentageTotalSize)
			layout.cells[row][column].childrenWidth, layout.cells[row][column].childrenHeight = 0, 0
		end
	end

	-- Подготавливаем объекты к расположению и подсчитываем тотальные размеры
	local child, layoutRow, layoutColumn, cell
	for i = 1, #layout.children do
		child = layout.children[i]
		
		if not child.hidden then
			layoutRow, layoutColumn = child.layoutRow, child.layoutColumn

			-- Проверка на позицию в сетке
			if layoutRow >= 1 and layoutRow <= #layout.rowSizes and layoutColumn >= 1 and layoutColumn <= #layout.columnSizes then
				cell = layout.cells[layoutRow][layoutColumn]
				-- Авто-фиттинг объектов
				if cell.horizontalFitting then
					child.width = number.round(layout.columnSizes[layoutColumn].calculatedSize - cell.horizontalFittingRemove)
				end

				if cell.verticalFitting then
					child.height = number.round(layout.rowSizes[layoutRow].calculatedSize - cell.verticalFittingRemove)
				end

				-- Направление и расчет размеров
				if cell.direction == GUI.DIRECTION_HORIZONTAL then
					cell.childrenWidth = cell.childrenWidth + child.width + cell.spacing
					cell.childrenHeight = math.max(cell.childrenHeight, child.height)
				else
					cell.childrenWidth = math.max(cell.childrenWidth, child.width)
					cell.childrenHeight = cell.childrenHeight + child.height + cell.spacing
				end
			else
				error("Layout child with index " .. i .. " has been assigned to cell (" .. layoutColumn .. "x" .. layoutRow .. ") out of layout grid range")
			end
		end
	end

	-- Высчитываем стартовую позицию объектов ячейки
	local x, y = 1, 1
	for row = 1, #layout.rowSizes do
		for column = 1, #layout.columnSizes do
			cell = layout.cells[row][column]
			cell.x, cell.y = GUI.getAlignmentCoordinates(
				x,
				y,
				layout.columnSizes[column].calculatedSize,
				layout.rowSizes[row].calculatedSize,
				cell.horizontalAlignment,
				cell.verticalAlignment,
				cell.childrenWidth - (cell.direction == GUI.DIRECTION_HORIZONTAL and cell.spacing or 0),
				cell.childrenHeight - (cell.direction == GUI.DIRECTION_VERTICAL and cell.spacing or 0)
			)

			-- Учитываем отступы от краев ячейки
			if cell.horizontalMargin ~= 0 or cell.verticalMargin ~= 0 then
				cell.x, cell.y = GUI.getMarginCoordinates(
					cell.x,
					cell.y,
					cell.horizontalAlignment,
					cell.verticalAlignment,
					cell.horizontalMargin,
					cell.verticalMargin
				)
			end

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

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

	-- Размещаем все объекты
	for i = 1, #layout.children do
		child = layout.children[i]
		
		if not child.hidden then
			cell = layout.cells[child.layoutRow][child.layoutColumn]
			
			child.localX, cell.localY = GUI.getAlignmentCoordinates(
				cell.x,
				cell.y,
				cell.childrenWidth,
				cell.childrenHeight,
				cell.horizontalAlignment,
				cell.verticalAlignment,
				child.width,
				child.height
			)

			if cell.direction == GUI.DIRECTION_HORIZONTAL then
				child.localX, child.localY = math.floor(cell.x), math.floor(cell.localY)
				cell.x = cell.x + child.width + cell.spacing
			else
				child.localX, child.localY = math.floor(child.localX), math.floor(cell.y)
				cell.y = cell.y + child.height + cell.spacing
			end
		end
	end
end

local function layoutSetPosition(layout, column, row, object)
	layoutCheckCell(layout, column, row)
	object.layoutRow = row
	object.layoutColumn = column

	return object
end

local function layoutSetDirection(layout, column, row, direction)
	layoutCheckCell(layout, column, row)
	layout.cells[row][column].direction = direction

	return layout
end

local function layoutSetSpacing(layout, column, row, spacing)
	layoutCheckCell(layout, column, row)
	layout.cells[row][column].spacing = spacing

	return layout
end

local function layoutSetAlignment(layout, column, row, horizontalAlignment, verticalAlignment)
	layoutCheckCell(layout, column, row)
	layout.cells[row][column].horizontalAlignment, layout.cells[row][column].verticalAlignment = horizontalAlignment, verticalAlignment

	return layout
end

local function layoutGetMargin(layout, column, row)
	layoutCheckCell(layout, column, row)

	return layout.cells[row][column].horizontalMargin, layout.cells[row][column].verticalMargin
end

local function layoutSetMargin(layout, column, row, horizontalMargin, verticalMargin)
	layoutCheckCell(layout, column, row)
	layout.cells[row][column].horizontalMargin = horizontalMargin
	layout.cells[row][column].verticalMargin = verticalMargin

	return layout
end

local function layoutNewCell()
	return {
		horizontalAlignment = GUI.ALIGNMENT_HORIZONTAL_CENTER,
		verticalAlignment = GUI.ALIGNMENT_VERTICAL_CENTER,
		horizontalMargin = 0,
		verticalMargin = 0,
		direction = GUI.DIRECTION_VERTICAL,
		spacing = 1
	}
end

local function layoutCalculatePercentageSize(changingExistent, array, index)
	if array[index].sizePolicy == GUI.SIZE_POLICY_RELATIVE then
		local allPercents, beforeFromIndexPercents = 0, 0
		for i = 1, #array do
			if array[i].sizePolicy == GUI.SIZE_POLICY_RELATIVE 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.SIZE_POLICY_RELATIVE and i ~= index then
				array[i].size = modifyer * array[i].size
			end
		end
	end
end

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

	return layout
end

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

	return layout
end

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

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

	return layout
end

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

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

	layoutCalculatePercentageSize(false, layout.rowSizes, #layout.rowSizes)

	return layout
end

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

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

	table.remove(layout.rowSizes, row)

	return layout
end

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

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

	table.remove(layout.columnSizes, column)

	return layout
end

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

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

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

	return layout
end

local function layoutDraw(layout)
	layout:update()
	containerDraw(layout)
	
	if layout.showGrid then
		local x, y = layout.x, layout.y
		for j = 1, #layout.columnSizes do
			for i = 1, #layout.rowSizes do
				screen.drawFrame(
					number.round(x),
					number.round(y),
					number.round(layout.columnSizes[j].calculatedSize),
					number.round(layout.rowSizes[i].calculatedSize),
					0xFF0000
				)
				y = y + layout.rowSizes[i].calculatedSize
			end
			x, y = x + layout.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.cells[row][column].direction == GUI.DIRECTION_HORIZONTAL then
				layout.width = layout.width + layout.children[i].width + layout.cells[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.cells[row][column].spacing
			end
		end
	end

	if layout.cells[row][column].direction == GUI.DIRECTION_HORIZONTAL then
		layout.width = layout.width - layout.cells[row][column].spacing
	else
		layout.height = layout.height - layout.cells[row][column].spacing
	end

	return layout
end

local function layoutSetFitting(layout, column, row, horizontal, vertical, horizontalRemove, verticalRemove)
	layoutCheckCell(layout, column, row)
	layout.cells[row][column].horizontalFitting = horizontal
	layout.cells[row][column].verticalFitting = vertical
	layout.cells[row][column].horizontalFittingRemove = horizontalRemove or 0
	layout.cells[row][column].verticalFittingRemove = verticalRemove or 0

	return layout
end

local function layoutAddChild(layout, object, ...)
	object.layoutRow = layout.defaultRow
	object.layoutColumn = layout.defaultColumn
	containerAddChild(layout, object, ...)

	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.setPosition = layoutSetPosition
	layout.setDirection = layoutSetDirection
	layout.setGridSize = layoutSetGridSize
	layout.setSpacing = layoutSetSpacing
	layout.setAlignment = layoutSetAlignment
	layout.setMargin = layoutSetMargin
	layout.getMargin = layoutGetMargin
	
	layout.fitToChildrenSize = layoutFitToChildrenSize
	layout.setFitting = layoutSetFitting

	layout.update = layoutUpdate
	layout.addChild = layoutAddChild
	layout.draw = layoutDraw

	layout:setGridSize(columnCount, rowCount)

	return layout
end

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

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

	if filesystemDialog.IOMode == GUI.IO_MODE_SAVE then
		filesystemDialog.submitButton.disabled = not filesystemDialog.input.text or filesystemDialog.input.text == ""
	else
		filesystemDialog.input.text = filesystemDialog.filesystemTree.selectedItem or ""
		filesystemDialog.submitButton.disabled = not filesystemDialog.filesystemTree.selectedItem
	end
	
	containerDraw(filesystemDialog)
	GUI.drawShadow(filesystemDialog.x, filesystemDialog.y, filesystemDialog.width, filesystemDialog.height, GUI.CONTEXT_MENU_SHADOW_TRANSPARENCY, true)

	return filesystemDialog
end

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

	if filesystemDialog.IOMode == GUI.IO_MODE_SAVE then
		filesystemDialog.filesystemTree.showMode = GUI.IO_MODE_DIRECTORY
		filesystemDialog.filesystemTree.selectionMode = GUI.IO_MODE_DIRECTORY
		filesystemDialog.input.disabled = false
		filesystemDialog.extensionComboBox.hidden = filesystemDialog.filesystemMode ~= GUI.IO_MODE_FILE or not filesystemDialog.filesystemTree.extensionFilters
	else
		if filesystemDialog.filesystemMode == GUI.IO_MODE_FILE then
			filesystemDialog.filesystemTree.showMode = GUI.IO_MODE_BOTH
			filesystemDialog.filesystemTree.selectionMode = GUI.IO_MODE_FILE
		else
			filesystemDialog.filesystemTree.showMode = GUI.IO_MODE_DIRECTORY
			filesystemDialog.filesystemTree.selectionMode = GUI.IO_MODE_DIRECTORY
		end

		filesystemDialog.input.disabled = true
		filesystemDialog.extensionComboBox.hidden = true
	end
end

local function filesystemDialogAddExtensionFilter(filesystemDialog, extension)
	filesystemDialog.extensionComboBox:addItem(extension)
	filesystemDialog.extensionComboBox.width = math.max(filesystemDialog.extensionComboBox.width, unicode.wlen(extension) + 3)
	filesystemDialog.extensionComboBox.localX = filesystemDialog.cancelButton.localX - filesystemDialog.extensionComboBox.width - 2
	filesystemDialog.filesystemTree:addExtensionFilter(extension)

	filesystemDialog:setMode(filesystemDialog.IOMode, filesystemDialog.filesystemMode)
end

local function filesystemDialogExpandPath(filesystemDialog, ...)
	filesystemDialog.filesystemTree:expandPath(...)
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, 1, 0, 0xE1E1E1, 0x3C3C3C, 0x3C3C3C, 0xE1E1E1, cancelButtonText))
	filesystemDialog.submitButton = filesystemDialog:addChild(GUI.adaptiveRoundedButton(1, height - 1, 1, 0, 0x3C3C3C, 0xE1E1E1, 0xE1E1E1, 0x3C3C3C, submitButtonText))
	filesystemDialog.submitButton.localX = filesystemDialog.width - filesystemDialog.submitButton.width - 1
	filesystemDialog.cancelButton.localX = filesystemDialog.submitButton.localX - filesystemDialog.cancelButton.width - 2

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

	filesystemDialog.input = filesystemDialog:addChild(GUI.input(2, height - 1, 1, 1, 0xE1E1E1, 0x696969, 0x969696, 0xE1E1E1, 0x3C3C3C, "", placeholderText))

	filesystemDialog.filesystemTree = filesystemDialog:addChild(GUI.filesystemTree(1, 1, width, height - 3, 0xE1E1E1, 0x3C3C3C, 0x3C3C3C, 0xA5A5A5, 0x3C3C3C, 0xE1E1E1, 0xB4B4B4, 0xA5A5A5, 0xC3C3C3, 0x4B4B4B))
	filesystemDialog.filesystemTree.workPath = path
	filesystemDialog.animationDuration = GUI.FILESYSTEM_DIALOG_ANIMATION_DURATION

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

	filesystemDialog.expandPath = filesystemDialogExpandPath
	filesystemDialog:setMode(GUI.IO_MODE_OPEN, GUI.IO_MODE_FILE)

	return filesystemDialog
end

local function filesystemDialogShow(filesystemDialog)
	filesystemDialog.filesystemTree:updateFileList()
	filesystemDialog:addAnimation(
		function(animation)
			filesystemDialog.localY = math.floor(1 + (1.0 - animation.position) * (-filesystemDialog.height))
		end,
		function(animation)
			animation:remove()
		end
	):start(filesystemDialog.animationDuration)

	return filesystemDialog
end

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

function GUI.addFilesystemDialog(parentContainer, addPanel, ...)
	local container = GUI.addBackgroundContainer(parentContainer, addPanel, false, nil)

	local filesystemDialog = container:addChild(GUI.filesystemDialog(1, 1, ...))
	filesystemDialog.localX = math.floor(container.width / 2 - filesystemDialog.width / 2)
	filesystemDialog.localY = -filesystemDialog.height

	local function onAnyTouch()
		container:remove()
		filesystemDialog.workspace:draw()
	end

	filesystemDialog.cancelButton.onTouch = function()
		onAnyTouch()

		if filesystemDialog.onCancel then
			filesystemDialog.onCancel()
		end
	end

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

		if filesystemDialog.onSubmit then
			filesystemDialog.onSubmit(path)
		end
	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)
	
	screen.drawRectangle(object.x, object.y, object.width - tipWidth, object.height, object.colors.background, object.colors.text, " ")
	screen.drawRectangle(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, " ")
	screen.drawText(object.x + object.width - math.floor(tipWidth / 2) - 1, y, object.pressed and object.colors.tipBackground or object.colors.tipText, "…")
	screen.drawText(math.ceil(object.x + object.height / 2), y, object.colors.text, text.limit(object.path or object.placeholderText, object.width - tipWidth - 2, "left"))

	return object
end

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

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

local function filesystemChooserEventHandler(workspace, object, e1)
	if e1 == "touch" then
		object.pressed = true
		workspace:draw()

		local filesystemDialog = GUI.addFilesystemDialog(workspace, false, 50, math.floor(workspace.height * 0.8), object.submitButtonText, object.cancelButtonText, object.placeholderText, object.filesystemDialogPath)

		for key in pairs(object.extensionFilters) do
			filesystemDialog:addExtensionFilter(key)
		end

		filesystemDialog:setMode(object.IOMode, object.filesystemMode)

		if object.path and #object.path > 0 then
			-- local path = object.path:gsub("/+", "/")
			filesystemDialog.filesystemTree.selectedItem = object.IOMode == GUI.IO_MODE_OPEN and object.path or filesystem.path(object.path)
			filesystemDialog.input.text = filesystem.name(object.path)
			filesystemDialog:expandPath(object.IOMode == GUI.IO_MODE_OPEN and filesystem.path(object.path) or filesystem.path(filesystem.path(object.path)))
		end
		
		filesystemDialog.onCancel = function()
			object.pressed = false
			workspace:draw()
		end

		filesystemDialog.onSubmit = function(path)
			object.path = path
			filesystemDialog.onCancel()
			if object.onSubmit then
				object.onSubmit(object.path)
			end
		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.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.IO_MODE_FILE
	object.IOMode = GUI.IO_MODE_OPEN
	object.extensionFilters = {}

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

	return object
end

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

local function resizerDraw(object)
	local horizontalMode, x, y, symbol = object.width >= object.height

	if horizontalMode then
		screen.drawText(object.x, math.floor(object.y + object.height / 2), object.colors.helper, string.rep("━", object.width))
		
		if object.lastTouchX then
			screen.drawText(object.lastTouchX, object.lastTouchY, object.colors.arrow, "↑")
		end
	else
		local x = math.floor(object.x + object.width / 2)
		local bufferWidth, bufferHeight, index = screen.getResolution()
		
		for i = object.y, object.y + object.height - 1 do
			if x >= 1 and x <= bufferWidth and i >= 1 and i <= bufferHeight then
				index = screen.getIndex(x, i)
				screen.rawSet(index, screen.rawGet(index), object.colors.helper, "┃")
			end
		end

		if object.lastTouchX then
			screen.drawText(object.lastTouchX - 1, object.lastTouchY, object.colors.arrow, "←→")
		end
	end
end

local function resizerEventHandler(workspace, object, e1, e2, e3, e4)
	if e1 == "touch" then
		object.lastTouchX, object.lastTouchY = math.ceil(e3), math.ceil(e4)
		workspace:draw()
	
	elseif e1 == "drag" and object.lastTouchX then
		e3, e4 = math.ceil(e3), math.ceil(e4)

		if object.onResize then
			object.onResize(e3 - object.lastTouchX, e4 - object.lastTouchY)
		end
		
		object.lastTouchX, object.lastTouchY = e3, e4
		workspace:draw()
	
	elseif e1 == "drop" then
		if object.onResizeFinished then
			object.onResizeFinished()
		end

		object.lastTouchX, object.lastTouchY = nil, nil
		workspace: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

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

local function scrollBarDraw(scrollBar)
	local isVertical = scrollBar.height > scrollBar.width
	local valuesDelta = scrollBar.maximumValue - scrollBar.minimumValue
	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 = screen.get(scrollBar.x, y)
				screen.set(scrollBar.x, y, background, y >= y1 and y <= y2 and scrollBar.colors.foreground or scrollBar.colors.background, "┃")
			end
		else
			screen.drawRectangle(scrollBar.x, scrollBar.y, scrollBar.width, scrollBar.height, scrollBar.colors.background, scrollBar.colors.foreground, " ")
			screen.drawRectangle(
				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 = screen.get(x, scrollBar.y)
				screen.set(x, scrollBar.y, background, x >= x1 and x <= x2 and scrollBar.colors.foreground or scrollBar.colors.background, "⠤")
			end
		else
			screen.drawRectangle(scrollBar.x, scrollBar.y, scrollBar.width, scrollBar.height, scrollBar.colors.background, scrollBar.colors.foreground, " ")
			screen.drawRectangle(
				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(workspace, object, e1, e2, e3, e4, e5, ...)
	local newValue = object.value

	if e1 == "touch" or e1 == "drag" then
		e3, e4 = math.ceil(e3), math.ceil(e4)

		if object.height > object.width then
			if e4 == object.y + object.height - 1 then
				newValue = object.maximumValue
			else
				newValue = object.minimumValue + (e4 - object.y) / object.height * (object.maximumValue - object.minimumValue)
			end
		else
			if e3 == object.x + object.width - 1 then
				newValue = object.maximumValue
			else
				newValue = object.minimumValue + (e3 - object.x) / object.width * (object.maximumValue - object.minimumValue)
			end
		end
	
	elseif e1 == "scroll" then
		if e5 == 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 e1 == "touch" or e1 == "drag" or e1 == "scroll" then
		object.value = newValue

		if object.onTouch then
			object.onTouch(workspace, object, e1, e2, e3, e4, e5, ...)
		end

		workspace: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

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

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
		screen.drawRectangle(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
			screen.drawRectangle(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
			screen.drawText(tree.x + tree.items[i].offset, y, arrowColor, tree.expandedItems[tree.items[i].definition] and "▽" or "▷")
		end

		screen.drawText(tree.x + tree.items[i].offset + 2, y, textColor, unicode.sub(text .. 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(workspace, tree, e1, e2, e3, e4, e5, ...)
	if e1 == "touch" then
		e3, e4 = math.ceil(e3), math.ceil(e4)

		local i = e4 - tree.y + tree.fromItem
		
		if tree.items[i] then
			if
				tree.items[i].expandable and
				(
					tree.selectionMode == GUI.IO_MODE_FILE or
					e3 >= tree.x + tree.items[i].offset - 1 and e3 <= 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

				if tree.onItemExpanded then
					tree.onItemExpanded(tree.items[i], e1, e2, e3, e4, e5, ...)
				end
			else
				if
					(
						tree.selectionMode == GUI.IO_MODE_BOTH or
						tree.selectionMode == GUI.IO_MODE_DIRECTORY and tree.items[i].expandable or
						tree.selectionMode == GUI.IO_MODE_FILE
					) and not tree.items[i].disabled
				then
					tree.selectedItem = tree.items[i].definition

					if tree.onItemSelected then
						tree.onItemSelected(tree.selectedItem, e1, e2, e3, e4, e5, ...)
					end
				end
			end

			workspace:draw()
		end
	
	elseif e1 == "scroll" then
		if e5 == 1 then
			if tree.fromItem > 1 then
				tree.fromItem = tree.fromItem - 1
				workspace:draw()
			end
		else
			if tree.fromItem < #tree.items then
				tree.fromItem = tree.fromItem + 1
				workspace:draw()
			end
		end
	end
end

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

	return item
end

function GUI.tree(x, y, width, height, backgroundColor, expandableColor, notExpandableColor, arrowColor, backgroundSelectedColor, anySelectionColor, arrowSelectionColor, disabledColor, scrollBarBackground, scrollBarForeground, showMode, selectionMode)
	local tree = GUI.object(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

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

local function filesystemTreeUpdateFileListRecursively(tree, path, offset)
	local list = filesystem.list(path)

	local i, expandables = 1, {}
	while i <= #list do
		if filesystem.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.IO_MODE_BOTH or tree.showMode == GUI.IO_MODE_DIRECTORY then
		for i = 1, #expandables do
			tree:addItem(filesystem.name(expandables[i]):sub(1, -2), 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.IO_MODE_BOTH or tree.showMode == GUI.IO_MODE_FILE then
		for i = 1, #list do
			tree:addItem(list[i], path .. list[i], offset, false, tree.extensionFilters and not tree.extensionFilters[filesystem.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

local function filesystemTreeExpandPath(tree, path)
	local blyadina = tree.workPath
	for pizda in path:gmatch("[^/]+") do
		blyadina = blyadina .. pizda .. "/"
		tree.expandedItems[blyadina] = true
	end
end

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

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

	return tree
end

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

local function textBoxUpdate(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 = text.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)
	object:update()

	if object.colors.background then
		screen.drawRectangle(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, line, 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
				line, textColor = text.limit(object.linesCopy[i], object.textWidth), object.colors.text
			elseif lineType == "table" then
				line, textColor = text.limit(object.linesCopy[i].text, object.textWidth), object.linesCopy[i].color
			else
				error("Unknown TextBox line type: " .. tostring(lineType))
			end

			x = GUI.getAlignmentCoordinates(
				object.x + object.offset.horizontal,
				1,
				object.textWidth,
				1,
				object.horizontalAlignment,
				object.verticalAlignment,
				unicode.wlen(line),
				1
			)

			screen.drawText(math.floor(x), y, textColor, line)
			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(workspace, object, e1, e2, e3, e4, e5)
	if e1 == "scroll" then
		if e5 == 1 then
			object:scrollUp()
		else
			object:scrollDown()
		end

		workspace:draw()
	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.colors = {
		text = textColor,
		background = backgroundColor
	}
	object.lines = lines
	object.currentLine = currentLine or 1
	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, 0x4B4B4B, 1, 1, 1, 1, 1, true)
	object.scrollBarEnabled = false
	
	object.eventHandler = textBoxScrollEventHandler
	object.draw = textBoxDraw
	object.update = textBoxUpdate

	object.setAlignment = GUI.setAlignment
	object:setAlignment(GUI.ALIGNMENT_HORIZONTAL_LEFT, GUI.ALIGNMENT_VERTICAL_TOP)
	object:update()

	return object
end

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

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)
	screen.drawText(x, y, color, text)
end

local function inputDraw(input)
	local background, foreground, transparency, text
	
	if input.workspace.focusedObject == input then
		background, transparency = input.colors.focused.background, input.colors.focused.transparency
		
		if input.text == "" then
			input.textCutFrom = 1
			foreground, text = input.colors.placeholderText, input.text
		else
			foreground = input.colors.focused.text
			
			if input.textMask then
				text = string.rep(input.textMask, unicode.len(input.text))
			else
				text = input.text
			end
		end
	else
		background, transparency = input.colors.default.background, input.colors.default.transparency
		if input.text == "" then
			input.textCutFrom = 1
			foreground, text = input.colors.placeholderText, input.placeholderText
		else
			foreground = input.colors.default.text
			if input.textMask then
				text = string.rep(input.textMask, unicode.len(input.text))
			else
				text = input.text
			end
		end
	end

	if background then
		screen.drawRectangle(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(
			text or "",
			input.textCutFrom,
			input.textCutFrom + input.width - 1 - input.textOffset * 2
		)
	)

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

local function inputCursorBlink(workspace, input, state)
	input.cursorBlinkState = state
	input.cursorBlinkUptime = computer.uptime()
	workspace:draw()
end

local function inputStopInput(input)
	input.workspace.capturedObject = nil
	input.workspace.focusedObject = nil

	if input.validator then
		if not input.validator(input.text) then
			input.text = input.startText
			input.startText = nil

			input:setCursorPosition(unicode.len(input.text) + 1)
		end
	end

	inputCursorBlink(input.workspace, input, false)

	if input.onInputFinished then
		input.onInputFinished(workspace, input)
	end
end

local function inputStartInput(input)
	input.startText = input.text
	input.workspace.capturedObject = input
	input.workspace.focusedObject = input

	if input.historyEnabled then
		input.historyIndex = input.historyIndex + 1
	end

	if input.eraseTextOnFocus then
		input.text = ""
	end
	
	input:setCursorPosition(input.cursorPosition)
	inputCursorBlink(input.workspace, input, true)
end

local function inputEventHandler(workspace, input, e1, e2, e3, e4, e5, e6, ...)
	local focused = workspace.focusedObject == input

	if e1 == "touch" or e1 == "drag" then
		if input:isPointInside(math.ceil(e3), math.ceil(e4)) then
			input:setCursorPosition(input.textCutFrom + math.ceil(e3) - input.x - input.textOffset)

			if focused then
				inputCursorBlink(workspace, input, true)
			else
				input:startInput()
			end
		else
			if focused then
				inputStopInput(input)
			end
		end

	elseif e1 == "key_down" and focused then
		-- Return
		if e4 == 28 then
			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

			inputStopInput(input)

			if input.onKeyDown then
				input.onKeyDown(workspace, input, e1, e2, e3, e4, e5, e6, ...)
			end

			return
		
		-- Arrows up/down/left/right
		elseif e4 == 200 then
			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)
			end
		
		elseif e4 == 208 then
			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)
			end
		
		elseif e4 == 203 then
			input:setCursorPosition(input.cursorPosition - 1)
		
		elseif e4 == 205 then	
			input:setCursorPosition(input.cursorPosition + 1)
		
		-- Backspace
		elseif e4 == 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)
		
		-- Delete
		elseif e4 == 211 then
			input.text = unicode.sub(input.text, 1, input.cursorPosition - 1) .. unicode.sub(input.text, input.cursorPosition + 1, -1)
		
		-- Home
		elseif e4 == 199 then
			input:setCursorPosition(1)
		
		-- End
		elseif e4 == 207 then 
			input:setCursorPosition(unicode.len(input.text) + 1)
		
		else
			local char = unicode.char(e3)
			
			if not keyboard.isControl(e3) then
				input.text = unicode.sub(input.text, 1, input.cursorPosition - 1) .. char .. unicode.sub(input.text, input.cursorPosition, -1)
				input:setCursorPosition(input.cursorPosition + 1)
			end
		end

		if input.onKeyDown then
			input.onKeyDown(workspace, input, e1, e2, e3, e4, e5, e6, ...)
		end

		inputCursorBlink(workspace, input, true)
	
	elseif e1 == "clipboard" and focused then
		input.text = unicode.sub(input.text, 1, input.cursorPosition - 1) .. e3 .. unicode.sub(input.text, input.cursorPosition, -1)
		input:setCursorPosition(input.cursorPosition + unicode.len(e3))
		
		inputCursorBlink(workspace, input, true)
	
	elseif not e1 and focused and computer.uptime() - input.cursorBlinkUptime > input.cursorBlinkDelay then
		inputCursorBlink(workspace, input, not input.cursorBlinkState)
	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.startInput = inputStartInput

	return input
end

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

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

	screen.drawRectangle(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
			screen.drawRectangle(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

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

		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 + 2
		object.scrollBar.value = object.fromItem
		object.scrollBar.shownValueCount = object.height

		object.scrollBar:draw()
	end
end

local function autoCompleteScroll(workspace, 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(workspace, object, e1, e2, e3, e4, e5, ...)
	if e1 == "touch" then
		object.selectedItem = math.ceil(e4) - object.y + object.fromItem
		workspace:draw()

		if object.onItemSelected then
			event.sleep(0.2)
			object.onItemSelected(workspace, object, e1, e2, e3, e4, e5, ...)
		end
	
	elseif e1 == "scroll" then
		autoCompleteScroll(workspace, object, -e5)
		workspace:draw()
	
	elseif e1 == "key_down" then
		if e4 == 28 then
			if object.onItemSelected then
				object.onItemSelected(workspace, object, e1, e2, e3, e4, e5, ...)
			end
		
		elseif e4 == 200 then
			object.selectedItem = object.selectedItem - 1
			if object.selectedItem < 1 then
				object.selectedItem = 1
			end

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

			workspace:draw()
		
		elseif e4 == 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(workspace, object, 1)
			end
			
			workspace: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, asKey)
	object:clear()
	
	if asKey then
		if text then
			for key in pairs(variants) do
				if key ~= text and key:match("^" .. text) then
					table.insert(object.items,key)
				end
			end
		else
			for key in pairs(variants) do
				table.insert(object.items, key)
			end
		end
	else
		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
	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

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

local function brailleCanvasDraw(brailleCanvas)
	local index, background, foreground, symbol
	for y = 1, brailleCanvas.height do
		for x = 1, brailleCanvas.width do
			index = screen.getIndex(brailleCanvas.x + x - 1, brailleCanvas.y + y - 1)
			background, foreground, symbol = screen.rawGet(index)
			screen.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)
	
	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 or brailleCanvas.pixels[yReal][xReal][9]
	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]
	)

	return brailleCanvas
end

local function brailleCanvasGet(brailleCanvas, x, y)
	local xReal, yReal = math.ceil(x / 2), math.ceil(y / 4)
	return brailleCanvas.pixels[yReal][xReal][(y - (yReal - 1) * 4 - 1) * 2 + x - (xReal - 1) * 2], brailleCanvas.pixels[yReal][xReal][9], brailleCanvas.pixels[yReal][xReal][10]
end

local function brailleCanvasFill(brailleCanvas, x, y, width, height, state, color)
	for j = y, y + height - 1 do
		for i = x, x + width - 1 do
			brailleCanvas:set(i, j, state, color)
		end
	end
end

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

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

	brailleCanvas.get = brailleCanvasGet
	brailleCanvas.set = brailleCanvasSet
	brailleCanvas.fill = brailleCanvasFill
	brailleCanvas.clear = brailleCanvasClear

	brailleCanvas.draw = brailleCanvasDraw

	brailleCanvas:clear()

	return brailleCanvas
end

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

local function paletteShow(palette)
	local workspace = GUI.workspace()
	
	workspace:addChild(palette)

	palette.submitButton.onTouch = function()
		workspace:stop()
	end
	palette.cancelButton.onTouch = palette.submitButton.onTouch

	workspace:draw()
	workspace:start()	

	return palette.color.integer
end

function GUI.palette(x, y, startColor)
	local palette = GUI.window(x, y, 71, 25)
	
	palette.color = {hsb = {}, rgb = {}}
	palette:addChild(GUI.panel(1, 1, palette.width, palette.height, 0xF0F0F0))
	
	local bigImage = palette:addChild(GUI.image(1, 1, image.create(50, 25)))
	local bigCrest = palette:addChild(GUI.object(1, 1, 5, 3))

	local function paletteDrawBigCrestPixel(x, y, symbol)
		local background, foreground = screen.get(x, y)
		local r, g, b = color.integerToRGB(background)
		screen.set(x, y, background, (r + g + b) / 3 >= 127 and 0x0 or 0xFFFFFF, symbol)
	end

	bigCrest.draw = function(object)
		paletteDrawBigCrestPixel(object.x, object.y + 1, "─")
		paletteDrawBigCrestPixel(object.x + 1, object.y + 1, "─")
		paletteDrawBigCrestPixel(object.x + 3, object.y + 1, "─")
		paletteDrawBigCrestPixel(object.x + 4, object.y + 1, "─")
		paletteDrawBigCrestPixel(object.x + 2, object.y, "│")
		paletteDrawBigCrestPixel(object.x + 2, object.y + 2, "│")
	end
	
	bigCrest.blockScreenEvents = false
	
	local miniImage = palette:addChild(GUI.image(53, 1, image.create(3, 25)))
	
	local miniCrest = palette:addChild(GUI.object(52, 1, 5, 1))
	
	miniCrest.draw = function(object)
		screen.drawText(object.x, object.y, 0x0, ">")
		screen.drawText(object.x + 4, object.y, 0x0, "<")
	end

	local colorPanel = palette:addChild(GUI.panel(58, 2, 12, 3, 0x0))
	palette.submitButton = palette:addChild(GUI.roundedButton(58, 6, 12, 1, 0x4B4B4B, 0xFFFFFF, 0x2D2D2D, 0xFFFFFF, "OK"))
	palette.cancelButton = palette:addChild(GUI.roundedButton(58, 8, 12, 1, 0xFFFFFF, 0x696969, 0x2D2D2D, 0xFFFFFF, "Cancel"))

	local function paletteRefreshBigImage()
		local saturationStep, brightnessStep, saturation, brightness = 1 / bigImage.width, 1 / bigImage.height, 0, 1
		
		for j = 1, bigImage.height do
			for i = 1, bigImage.width do
				image.set(bigImage.image, i, j, color.optimize(color.HSBToInteger(palette.color.hsb.hue, saturation, brightness)), 0x0, 0x0, " ")
				saturation = saturation + saturationStep
			end
			
			saturation, brightness = 0, brightness - brightnessStep
		end
	end

	local function paletteRefreshMiniImage()
		local hueStep, hue = 360 / miniImage.height, 0
		
		for j = 1, miniImage.height do
			for i = 1, miniImage.width do
				image.set(miniImage.image, i, j, color.optimize(color.HSBToInteger(hue, 1, 1)), 0x0, 0, " ")
			end
			
			hue = hue + hueStep
		end
	end

	local function paletteUpdateCrestsCoordinates()
		bigCrest.localX = math.floor((bigImage.width - 1) * palette.color.hsb.saturation) - 1
		bigCrest.localY = math.floor((bigImage.height - 1) - (bigImage.height - 1) * palette.color.hsb.brightness)
		miniCrest.localY = math.ceil(palette.color.hsb.hue / 360 * miniImage.height + 0.5)
	end

	local inputs

	local function paletteUpdateInputs()
		inputs[1].text = tostring(palette.color.rgb.red)
		inputs[2].text = tostring(palette.color.rgb.green)
		inputs[3].text = tostring(palette.color.rgb.blue)
		inputs[4].text = tostring(math.floor(palette.color.hsb.hue))
		inputs[5].text = tostring(math.floor(palette.color.hsb.saturation * 100))
		inputs[6].text = tostring(math.floor(palette.color.hsb.brightness * 100))
		inputs[7].text = string.format("%06X", palette.color.integer)
		colorPanel.colors.background = palette.color.integer
	end

	local function paletteSwitchColorFromHex(hex)
		palette.color.integer = hex
		palette.color.rgb.red, palette.color.rgb.green, palette.color.rgb.blue = color.integerToRGB(hex)
		palette.color.hsb.hue, palette.color.hsb.saturation, palette.color.hsb.brightness = color.RGBToHSB(palette.color.rgb.red, palette.color.rgb.green, palette.color.rgb.blue)
		paletteUpdateInputs()
	end

	local function paletteSwitchColorFromHsb(hue, saturation, brightness)
		palette.color.hsb.hue, palette.color.hsb.saturation, palette.color.hsb.brightness = hue, saturation, brightness
		palette.color.rgb.red, palette.color.rgb.green, palette.color.rgb.blue = color.HSBToRGB(hue, saturation, brightness)
		palette.color.integer = color.RGBToInteger(palette.color.rgb.red, palette.color.rgb.green, palette.color.rgb.blue)
		paletteUpdateInputs()
	end

	local function paletteSwitchColorFromRgb(red, green, blue)
		palette.color.rgb.red, palette.color.rgb.green, palette.color.rgb.blue = red, green, blue
		palette.color.hsb.hue, palette.color.hsb.saturation, palette.color.hsb.brightness = color.RGBToHSB(red, green, blue)
		palette.color.integer = color.RGBToInteger(red, green, blue)
		paletteUpdateInputs()
	end

	local function onAnyInputFinished()
		paletteRefreshBigImage()
		paletteUpdateCrestsCoordinates()
		palette.workspace:draw()
	end

	local function onHexInputFinished()
		paletteSwitchColorFromHex(tonumber("0x" .. inputs[7].text))
		onAnyInputFinished()
	end

	local function onRgbInputFinished()
		paletteSwitchColorFromRgb(tonumber(inputs[1].text), tonumber(inputs[2].text), tonumber(inputs[3].text))
		onAnyInputFinished()
	end

	local function onHsbInputFinished()
		paletteSwitchColorFromHsb(tonumber(inputs[4].text), tonumber(inputs[5].text) / 100, tonumber(inputs[6].text) / 100)
		onAnyInputFinished()
	end

	local function rgbValidaror(text)
		local number = tonumber(text) if number and number >= 0 and number <= 255 then return true end
	end

	local function hValidator(text)
		local number = tonumber(text) if number and number >= 0 and number <= 359 then return true end
	end

	local function sbValidator(text)
		local number = tonumber(text) if number and number >= 0 and number <= 100 then return true end
	end

	local function hexValidator(text)
		if string.match(text, "^[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]$") then
			return true
		end
	end

	inputs = {
		{ shortcut = "R:", validator = rgbValidaror, onInputFinished = onRgbInputFinished },
		{ shortcut = "G:", validator = rgbValidaror, onInputFinished = onRgbInputFinished },
		{ shortcut = "B:", validator = rgbValidaror, onInputFinished = onRgbInputFinished },
		{ shortcut = "H:", validator = hValidator,   onInputFinished = onHsbInputFinished },
		{ shortcut = "S:", validator = sbValidator,  onInputFinished = onHsbInputFinished },
		{ shortcut = "L:", validator = sbValidator,  onInputFinished = onHsbInputFinished },
		{ shortcut = "0x", validator = hexValidator, onInputFinished = onHexInputFinished }
	}

	local y = 10
	for i = 1, #inputs do
		palette:addChild(GUI.label(58, y, 2, 1, 0x0, inputs[i].shortcut))
		
		local validator, onInputFinished = inputs[i].validator, inputs[i].onInputFinished
		inputs[i] = palette:addChild(GUI.input(61, y, 9, 1, 0xFFFFFF, 0x696969, 0x696969, 0xFFFFFF, 0x0, "", "", true))
		inputs[i].validator = validator
		inputs[i].onInputFinished = onInputFinished
		
		y = y + 2
	end
	
	local paletteConfigPath = paths.user.applicationData .. "GUI/Palette.cfg"
	
	local favourites
	if filesystem.exists(paletteConfigPath) then
		favourites = filesystem.readTable(paletteConfigPath)
	else
		favourites = {}
		for i = 1, 6 do favourites[i] = color.HSBToInteger(math.random(0, 360), 1, 1) end
		filesystem.writeTable(paletteConfigPath, favourites)
	end

	local favouritesContainer = palette:addChild(GUI.container(58, 24, 12, 1))
	for i = 1, #favourites do
		favouritesContainer:addChild(GUI.button(i * 2 - 1, 1, 2, 1, favourites[i], 0x0, 0x0, 0x0, " ")).onTouch = function(workspace)
			paletteSwitchColorFromHex(favourites[i])
			paletteRefreshBigImage()
			paletteUpdateCrestsCoordinates()
			workspace:draw()
		end
	end
	
	palette:addChild(GUI.button(58, 25, 12, 1, 0xFFFFFF, 0x4B4B4B, 0x2D2D2D, 0xFFFFFF, "+")).onTouch = function(workspace)
		local favouriteExists = false
		for i = 1, #favourites do
			if favourites[i] == palette.color.integer then
				favouriteExists = true
				break
			end
		end
		
		if not favouriteExists then
			table.insert(favourites, 1, palette.color.integer)
			table.remove(favourites, #favourites)
			for i = 1, #favourites do
				favouritesContainer.children[i].colors.default.background = favourites[i]
				favouritesContainer.children[i].colors.pressed.background = 0x0
			end
			
			filesystem.writeTable(paletteConfigPath, favourites)

			workspace:draw()
		end
	end

	bigImage.eventHandler = function(workspace, object, e1, e2, e3, e4)
		if e1 == "touch" or e1 == "drag" then
			e3, e4 = math.ceil(e3), math.ceil(e4)

			bigCrest.localX, bigCrest.localY = e3 - palette.x - 1, e4 - palette.y
			paletteSwitchColorFromHex(select(3, component.invoke(screen.getGPUAddress(), "get", e3, e4)))
			
			workspace:draw()
		end
	end
	
	miniImage.eventHandler = function(workspace, object, e1, e2, e3, e4)
		if e1 == "touch" or e1 == "drag" then
			e4 = math.ceil(e4)

			miniCrest.localY = e4 - palette.y + 1
			paletteSwitchColorFromHsb((e4 - miniImage.y) * 360 / miniImage.height, palette.color.hsb.saturation, palette.color.hsb.brightness)
			paletteRefreshBigImage()
			
			workspace:draw()
		end
	end

	palette.show = paletteShow

	paletteSwitchColorFromHex(startColor)
	paletteUpdateCrestsCoordinates()
	paletteRefreshBigImage()
	paletteRefreshMiniImage()

	return palette
end

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

local function textUpdate(object)
	object.width = unicode.wlen(object.text)
	
	return object
end

local function textDraw(object)
	object:update()
	screen.drawText(object.x, object.y, object.color, object.text, object.transparency)

	return object
end

function GUI.text(x, y, color, text, transparency)
	local object = GUI.object(x, y, 1, 1)

	object.text = text
	object.color = color
	object.transparency = transparency
	object.update = textUpdate
	object.draw = textDraw
	object:update()

	return object
end

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

function GUI.addBackgroundContainer(parentContainer, addPanel, addLayout, title)
	local container = parentContainer:addChild(GUI.container(1, 1, parentContainer.width, parentContainer.height))
	
	if addPanel then
		container.panel = container:addChild(GUI.panel(1, 1, container.width, container.height, GUI.BACKGROUND_CONTAINER_PANEL_COLOR, GUI.BACKGROUND_CONTAINER_PANEL_TRANSPARENCY))
		container.panel.eventHandler = function(parentContainer, object, e1)
			if e1 == "touch" then
				container:remove()
				parentContainer:draw()
			end
		end
	end

	if addLayout then
		container.layout = container:addChild(GUI.layout(1, 1, container.width, container.height, 1, 1))

		if title then
			container.label = container.layout:addChild(GUI.label(1, 1, 1, 1, GUI.BACKGROUND_CONTAINER_TITLE_COLOR, title)):setAlignment(GUI.ALIGNMENT_HORIZONTAL_CENTER, GUI.ALIGNMENT_VERTICAL_TOP)
		end
	end

	return container
end

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

local function listUpdate(list)
	local step, child = false

	for i = 1, #list.children do
		child = list.children[i]
		-- Жмяканье пизды
		child.pressed = i == list.selectedItem
		
		-- Цвет залупы
		if step then
			child.colors.default = list.colors.alternative
		else
			child.colors.default = list.colors.default
		end

		child.colors.pressed, step = list.colors.selected, not step
		
		-- Размеры хуйни
		if list.cells[1][1].direction == GUI.DIRECTION_HORIZONTAL then
			if list.offsetMode then
				child.width, child.height = list.itemSize * 2 + unicode.wlen(child.text), list.height
			else
				child.width, child.height = list.itemSize, list.height
			end
		else
			if list.offsetMode then
				child.width, child.height = list.width, list.itemSize * 2 + 1
			else
				child.width, child.height = list.width, list.itemSize
			end
		end
	end

	layoutUpdate(list)
end

local function listItemEventHandler(workspace, item, e1, ...)
	if e1 == "touch" or e1 == "drag" then
		item.parent.selectedItem = item:indexOf()
		item.parent:update()
		workspace:draw()

		if item.onTouch then
			item.onTouch(workspace, item, e1, ...)
		end
	end
end

local function listAddItem(list, text)
	local item = list:addChild(pressable(1, 1, 1, 1, 0, 0, 0, 0, 0, 0, text))
	
	item.switchMode = true
	item.eventHandler = listItemEventHandler

	return item
end

local function listSetAlignment(list, ...)
	layoutSetAlignment(list, 1, 1, ...)
	return list
end

local function listSetSpacing(list, ...)
	layoutSetSpacing(list, 1, 1, ...)
	return list
end

local function listSetDirection(list, ...)
	layoutSetDirection(list, 1, 1, ...)
	return list
end

local function listSetFitting(list, ...)
	layoutSetFitting(list, 1, 1, ...)
	return list
end

local function listSetMargin(list, ...)
	layoutSetMargin(list, 1, 1, ...)
	return list
end

local function listGetMargin(list, ...)
	return layoutGetMargin(list, 1, 1, ...)
end

local function listGetItem(list, what)
	if type(what) == "number" then
		return list.children[what]
	else
		for i = 1, #list.children do
			if list.children[i].text == what then
				return list.children[i], i
			end
		end
	end
end

local function listCount(list)
	return #list.children
end

local function listDraw(list)
	if list.colors.default.background then
		screen.drawRectangle(list.x, list.y, list.width, list.height, list.colors.default.background, list.colors.default.text, " ")
	end

	layoutDraw(list)
end

function GUI.list(x, y, width, height, itemSize, spacing, backgroundColor, textColor, backgroundAlternatingColor, textAlternatingColor, backgroundSelectedColor, textSelectedColor, offsetMode)
	local list = GUI.layout(x, y, width, height, 1, 1)

	list.colors = {
		default = {
			background = backgroundColor,
			text = textColor
		},
		alternative = {
			background = backgroundAlternatingColor,
			text = textAlternatingColor
		},
		selected = {
			background = backgroundSelectedColor,
			text = textSelectedColor
		},
	}

	list.blockScreenEvents = true
	list.selectedItem = 1
	list.offsetMode = offsetMode
	list.itemSize = itemSize
	
	list.addItem = listAddItem
	list.getItem = listGetItem
	list.count = listCount
	list.setAlignment = listSetAlignment
	list.setSpacing = listSetSpacing
	list.setDirection = listSetDirection
	list.setFitting = listSetFitting
	list.setMargin = listSetMargin
	list.getMargin = listGetMargin
	list.update = listUpdate
	list.draw = listDraw

	list:setAlignment(GUI.ALIGNMENT_HORIZONTAL_LEFT, GUI.ALIGNMENT_VERTICAL_TOP)
	list:setSpacing(spacing)
	list:setDirection(GUI.DIRECTION_VERTICAL)

	return list
end

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

local function keyAndValueUpdate(object)
	object.keyLength, object.valueLength = unicode.wlen(object.key), unicode.wlen(object.value)
	object.width = object.keyLength + object.valueLength
end

local function keyAndValueDraw(object)
	keyAndValueUpdate(object)
	screen.drawText(object.x, object.y, object.colors.key, object.key)
	screen.drawText(object.x + object.keyLength, object.y, object.colors.value, object.value)
end

function GUI.keyAndValue(x, y, keyColor, valueColor, key, value)
	local object = GUI.object(x, y, 1, 1)
	
	object.colors = {
		key = keyColor,
		value = valueColor
	}
	object.key = key
	object.value = value

	object.update = keyAndValueUpdate
	object.draw = keyAndValueDraw

	object:update()

	return object
end

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

function GUI.highlightString(x, y, fromChar, indentationWidth, patterns, colorScheme, s)	
	local stringLength, x1, y1, x2, y2 = unicode.len(s), screen.getDrawLimit()

	fromChar = fromChar or 1
	if x < x1 then
		fromChar = fromChar + x1 - x
		x = x1
	end

	-- local toChar, endX = stringLength, x + stringLength - 1
	-- if endX > x2 then
	-- 	toChar = toChar - endX + x2
	-- end
	local toChar = fromChar + x2 - x
	if toChar > stringLength then
		toChar = stringLength
	end

	local counter, symbols, colors, bufferIndex, newFrameBackgrounds, newFrameForegrounds, newFrameSymbols, searchFrom, starting, ending = indentationWidth, {}, {}, screen.getIndex(x, y), screen.getNewFrameTables()

	-- Пидорасим на символы
	for i = fromChar, toChar do
		symbols[i] = unicode.sub(s, i, i)
	end

	-- Вгоняем в цветовую карту синтаксическую подсветку
	for j = 1, #patterns, 4 do
		searchFrom = 1
		
		while true do
			starting, ending = text.unicodeFind(s, patterns[j], searchFrom)
			
			if starting then
				for i = starting + patterns[j + 2], ending - patterns[j + 3] do
					colors[i] = colorScheme[patterns[j + 1]]
				end
			else
				break
			end

			searchFrom = ending + 1 - patterns[j + 3]
		end
	end

	-- Ебошим индентейшны
	for i = fromChar, toChar do
		if symbols[i] == " " then
			colors[i] = colorScheme.indentation
			
			if counter == indentationWidth then
				symbols[i], counter = "│", 0
			end

			counter = counter + 1
		else
			break
		end
	end

	-- Рисуем текст
	for i = fromChar, toChar do
		newFrameForegrounds[bufferIndex], newFrameSymbols[bufferIndex] = colors[i] or colorScheme.text, symbols[i] or " "
		bufferIndex = bufferIndex + 1
	end
end

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

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

	if item.isTextItem then
		local iconColor = item.color or item.parent.parent.colors.default.icon
		local textColor = item.color or item.parent.parent.colors.default.text

		if item.pressed then
			iconColor = item.parent.parent.colors.selected.icon
			textColor = item.parent.parent.colors.selected.text

			screen.drawRectangle(item.x, yText, item.width, 1, item.parent.parent.colors.selected.background, textColor, " ")

			if item.height > 1 then
				screen.drawText(item.x, yText - 1, item.parent.parent.colors.selected.background, string.rep("⣤", item.width))
				screen.drawText(item.x, yText + item.height - 1, item.parent.parent.colors.selected.background, string.rep("⠛", item.width))
			end
		
		elseif item.disabled then
			iconColor = item.parent.parent.colors.disabled.icon
			textColor = item.parent.parent.colors.disabled.text
		end

		-- Icon & text
		if item.icon then
			screen.drawText(item.x + item.height, yText, iconColor, item.icon)
			screen.drawText(item.x + item.height * 2 + 2, yText, textColor, item.text)
		else
			screen.drawText(item.x + item.height, yText, textColor, item.text)
		end

		-- shortcut
		if item.shortcut then
			screen.drawText(item.x + item.width - unicode.wlen(item.shortcut) - item.height, yText, textColor, item.shortcut)
		end
	else
		screen.drawText(item.x, yText, item.parent.parent.colors.separator, string.rep(item.parent.parent.itemHeight % 2 == 0 and "▁" or "─", item.width))
	end

	return item
end

local function dropDownMenuReleaseItems(menu)
	for i = 1, #menu.itemsContainer.children do
		menu.itemsContainer.children[i].pressed = false
	end

	return menu
end

local function dropDownMenuItemEventHandler(workspace, object, e1, ...)
	if e1 == "touch" then
		if object.isTextItem and not object.pressed then
			dropDownMenuReleaseItems(object.parent.parent)
			
			if #object.parent.parent.subMenus then
				for i, subMenu in ipairs(object.parent.parent.subMenus) do
			 		subMenu:remove()
			 		table.remove(object.parent.parent.subMenus, i)
			 	end
			end

			object.pressed = true
			workspace:draw()

			if object.subMenu then
				object.parent.parent.parent:addChild(object.subMenu:releaseItems())
				object.subMenu.localX = object.parent.parent.localX + object.parent.parent.width
				object.subMenu.localY = object.parent.parent.localY + object.localY - 1
				
				if screen.getWidth() - object.parent.parent.localX - object.parent.parent.width + 1 < object.subMenu.width then
					object.subMenu.localX = object.parent.parent.localX - object.subMenu.width
					object.parent.parent:moveToFront()
				end
				
				table.insert(object.parent.parent.subMenus, object.subMenu)
				
				workspace:draw()
			else
				event.sleep(0.2)

				object.parent.parent.parent:remove()
				
				local objectIndex = object:indexOf()
				
				for i = 2, #object.parent.parent.parent.children do
					if object.parent.parent.parent.children[i].onMenuClosed then
						object.parent.parent.parent.children[i].onMenuClosed(objectIndex)
					end
				end

				if object.onTouch then
					object.onTouch(workspace, object, e1, ...)
				end

				workspace:draw()
			end
		end
	end
end

local function dropDownMenuGetHeight(menu)
	local height = menu.itemHeight % 2 == 0 and 1 or 0

	for i = 1, #menu.itemsContainer.children do
		height = height + menu.itemsContainer.children[i].height
	end

	return height
end

local function dropDownMenuReposition(menu)
	menu.itemsContainer.width, menu.itemsContainer.height = menu.width, menu.height
	menu.prevButton.width, menu.nextButton.width = menu.width, menu.width
	menu.nextButton.localY = menu.height

	local y = menu.itemsContainer.children[1].localY
	
	for i = 1, #menu.itemsContainer.children do
		menu.itemsContainer.children[i].localY = y
		menu.itemsContainer.children[i].width = menu.itemsContainer.width
		
		y = y + menu.itemsContainer.children[i].height
	end

	menu.prevButton.hidden = menu.itemsContainer.children[1].localY >= 1
	menu.nextButton.hidden = menu.itemsContainer.children[#menu.itemsContainer.children].localY + menu.itemsContainer.children[#menu.itemsContainer.children].height - 1 <= menu.height
end

local function dropDownMenuUpdate(menu)
	if #menu.itemsContainer.children > 0 then
		menu.height = math.min(dropDownMenuGetHeight(menu), menu.maximumHeight, screen.getHeight() - menu.y)

		dropDownMenuReposition(menu)
	end
end

local function dropDownMenuRemoveItem(menu, index)
	table.remove(menu.itemsContainer.children, index)

	menu:update()

	return menu
end

local function dropDownMenuAddItem(menu, a1, a2, a3, a4, a5)
	local item = menu.itemsContainer:addChild(GUI.object(1, 1, 1, menu.itemHeight))

	if type(a1) == "string" and type(a2) == "string" then
		item.icon = a1
		item.text = a2
		item.disabled = a3
		item.shortcut = a4
		item.color = a5
	else
		item.text = a1
		item.disabled = a2
		item.shortcut = a3
		item.color = a4
	end

	item.draw = dropDownMenuItemDraw
	item.eventHandler = dropDownMenuItemEventHandler
	item.isTextItem = true

	menu:update()

	return item
end

local function dropDownMenuAddSeparator(menu)
	local item = menu.itemsContainer:addChild(GUI.object(1, 1, 1, 1))
	
	item.draw = dropDownMenuItemDraw
	item.eventHandler = dropDownMenuItemEventHandler

	menu:update()

	return item
end

local function dropDownMenuScrollDown(workspace, menu)
	local limit, first = 1, menu.itemsContainer.children[1]

	first.localY = first.localY + menu.scrollSpeed
	
	if first.localY > limit then
		first.localY = limit
	end

	dropDownMenuReposition(menu)
	workspace:draw()
end

local function dropDownMenuScrollUp(workspace, menu)
	local limit, first = -(dropDownMenuGetHeight(menu) - menu.height - 1), menu.itemsContainer.children[1]

	first.localY = first.localY - menu.scrollSpeed

	if first.localY < limit then
		first.localY = limit
	end

	dropDownMenuReposition(menu)
	workspace:draw()
end

local function dropDownMenuEventHandler(workspace, menu, e1, e2, e3, e4, e5)
	if e1 == "scroll" then
		if e5 == 1 then
			dropDownMenuScrollDown(workspace, menu)
		else
			dropDownMenuScrollUp(workspace, menu)
		end
	end
end

local function dropDownMenuPrevButtonOnTouch(workspace, button)
	dropDownMenuScrollDown(workspace, button.parent)
end

local function dropDownMenuNextButtonOnTouch(workspace, button)
	dropDownMenuScrollUp(workspace, button.parent)
end

local function dropDownMenuDraw(menu)
	screen.drawRectangle(menu.x, menu.y, menu.width, menu.height, menu.colors.default.background, menu.colors.default.text, " ", menu.colors.transparency.background)
	GUI.drawShadow(menu.x, menu.y, menu.width, menu.height, menu.colors.transparency.shadow, true)
	containerDraw(menu)
end

local function dropDownMenuBackgroundObjectEventHandler(workspace, object, e1)
	if e1 == "touch" then
		for i = 2, #object.parent.children do
			if object.parent.children[i].onMenuClosed then
				object.parent.children[i].onMenuClosed()
			end
		end

		object.parent:remove()
		workspace:draw()
	end
end

local function dropDownMenuAdd(parentContainer, menu)
	local container = parentContainer:addChild(GUI.container(1, 1, parentContainer.width, parentContainer.height))
	container:addChild(GUI.object(1, 1, container.width, container.height)).eventHandler = dropDownMenuBackgroundObjectEventHandler
	
	return container:addChild(menu:releaseItems())
end

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

	menu.scrollSpeed = 1

	menu.itemsContainer = menu:addChild(GUI.container(1, 1, menu.width, menu.height))

	-- RIP ▲▼
	-- ⬆⬇
	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.transparency, menu.nextButton.colors.transparency = backgroundTransparency, backgroundTransparency
	menu.prevButton.onTouch = dropDownMenuPrevButtonOnTouch
	menu.nextButton.onTouch = dropDownMenuNextButtonOnTouch

	menu.releaseItems = dropDownMenuReleaseItems
	menu.itemHeight = itemHeight
	menu.addSeparator = dropDownMenuAddSeparator
	menu.addItem = dropDownMenuAddItem
	menu.removeItem = dropDownMenuRemoveItem
	menu.draw = dropDownMenuDraw
	menu.maximumHeight = maximumHeight
	menu.eventHandler = dropDownMenuEventHandler
	menu.update = dropDownMenuUpdate

	menu.subMenus = {}	
	return menu
end

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

local function contextMenuUpdate(menu)
	if #menu.itemsContainer.children > 0 then
		local widestItem, widestShortcut, haveIcon, item = 0, 0, false
		
		for i = 1, #menu.itemsContainer.children do
			item = menu.itemsContainer.children[i]

			if item.isTextItem then
				if item.icon then
					haveIcon = true
				end

				widestItem = math.max(widestItem, unicode.wlen(item.text))
				
				if item.shortcut then
					widestShortcut = math.max(widestShortcut, unicode.wlen(item.shortcut))
				end
			end
		end

		menu.width =
			menu.itemHeight * 2 +
			(haveIcon and menu.itemHeight + 2 or 0) +
			widestItem +
			(widestShortcut > 0 and widestShortcut + menu.itemHeight + 1 or 0)
		
		menu.height = math.min(dropDownMenuGetHeight(menu), menu.maximumHeight)
		
		dropDownMenuReposition(menu)

		local bufferWidth, bufferHeight = screen.getResolution()
		
		if menu.x + menu.width + 1 >= bufferWidth then
			menu.localX = bufferWidth - menu.width - 1
		end
		
		if menu.y + menu.height >= bufferHeight then
			menu.localY = bufferHeight - menu.height
		end
	end
end

local contextMenuCreate, contextMenuaddSubMenuItem

contextMenuaddSubMenuItem = function(menu, a1, a2, a3)
	local item

	if type(a1) == "string" and type(a2) == "string" then
		item = menu:addItem(a1, a2, a3, "►")
	else
		item = menu:addItem(a1, a2, "►")
	end

	item.subMenu = contextMenuCreate(1, 1)
	item.subMenu.colors = menu.colors
	
	return item.subMenu
end

contextMenuCreate = function(x, y, backgroundColor, textColor, backgroundPressedColor, textPressedColor, disabledColor, separatorColor, backgroundTransparency, shadowTransparency)
	local menu = GUI.dropDownMenu(
		x,
		y,
		1,
		math.ceil(screen.getHeight() * 0.7),
		2,
		GUI.CONTEXT_MENU_DEFAULT_BACKGROUND_COLOR,
		GUI.CONTEXT_MENU_DEFAULT_ICON_COLOR,
		GUI.CONTEXT_MENU_DEFAULT_TEXT_COLOR,

		GUI.CONTEXT_MENU_PRESSED_BACKGROUND_COLOR,
		GUI.CONTEXT_MENU_PRESSED_ICON_COLOR,
		GUI.CONTEXT_MENU_PRESSED_TEXT_COLOR,

		GUI.CONTEXT_MENU_DISABLED_ICON_COLOR,
		GUI.CONTEXT_MENU_DISABLED_TEXT_COLOR,
		
		GUI.CONTEXT_MENU_SEPARATOR_COLOR,
		GUI.CONTEXT_MENU_BACKGROUND_TRANSPARENCY,
		GUI.CONTEXT_MENU_SHADOW_TRANSPARENCY
	)

	menu.update = contextMenuUpdate
	menu.addSubMenuItem = contextMenuaddSubMenuItem

	return menu
end

function GUI.addContextMenu(parentContainer, arg1, ...)
	if type(arg1) == "table" then
		return dropDownMenuAdd(parentContainer, arg1, ...)
	else
		return dropDownMenuAdd(parentContainer, contextMenuCreate(arg1, ...))
	end
end

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

local function comboBoxDraw(object)
	local arrowSize = object.height * 2 - 1
	local width = object.width - arrowSize

	-- Background
	screen.drawRectangle(object.x, object.y, width, object.height, object.colors.default.background, object.colors.default.text, " ")
	
	-- Item
	if object.dropDownMenu.itemsContainer.children[object.selectedItem] then
		screen.drawText(
			math.ceil(object.x + object.height / 2),
			math.floor(object.y + object.height / 2),
			object.colors.default.text,
			text.limit(object.dropDownMenu.itemsContainer.children[object.selectedItem].text, object.width - object.height - 2, "right")
		)
	end

	-- Arrow
	width = object.x + width
	screen.drawRectangle(width, object.y, arrowSize, object.height, object.colors.arrow.background, object.colors.arrow.text, " ")
	screen.drawText(math.floor(width + arrowSize / 2), math.floor(object.y + object.height / 2), object.colors.arrow.text, object.pressed and "⬆" or "⬇")

	return object
end

local function comboBoxGetItem(object, what)
	if type(what) == "number" then
		return object.dropDownMenu.itemsContainer.children[what]
	else
		local children = object.dropDownMenu.itemsContainer.children
		
		for i = 1, #children do
			if children[i].text == what then
				return children[i], i
			end
		end
	end
end

local function comboBoxRemoveItem(object, index)
	object.dropDownMenu:removeItem(index)
	if object.selectedItem > #object.dropDownMenu.itemsContainer.children then
		object.selectedItem = #object.dropDownMenu.itemsContainer.children
	end
end

local function comboBoxCount(object)
	return #object.dropDownMenu.itemsContainer.children
end

local function comboBoxClear(object)
	object.dropDownMenu.itemsContainer:removeChildren()
	object.selectedItem = 1

	return object
end

local function comboBoxEventHandler(workspace, object, e1, ...)
	if e1 == "touch" and #object.dropDownMenu.itemsContainer.children > 0 then
		object.pressed = true
		object.dropDownMenu.x, object.dropDownMenu.y, object.dropDownMenu.width = object.x, object.y + object.height, object.width
		object.dropDownMenu:update()
		dropDownMenuAdd(workspace, object.dropDownMenu)
		workspace:draw()
	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, height, backgroundColor, textColor, arrowBackgroundColor, arrowTextColor)
	local comboBox = GUI.object(x, y, width, height)
	
	comboBox.colors = {
		default = {
			background = backgroundColor,
			text = textColor
		},
		selected = {
			background = GUI.CONTEXT_MENU_PRESSED_BACKGROUND_COLOR,
			text = GUI.CONTEXT_MENU_PRESSED_TEXT_COLOR
		},
		arrow = {
			background = arrowBackgroundColor,
			text = arrowTextColor
		}
	}

	comboBox.dropDownMenu = GUI.dropDownMenu(
		1,
		1,
		1,
		math.ceil(screen.getHeight() * 0.5),
		height == 1 and 1 or 2,
		comboBox.colors.default.background, 
		comboBox.colors.default.text, 
		comboBox.colors.default.text, 
		comboBox.colors.selected.background,
		comboBox.colors.selected.text,
		comboBox.colors.selected.text,
		GUI.CONTEXT_MENU_DISABLED_ICON_COLOR,
		GUI.CONTEXT_MENU_DISABLED_TEXT_COLOR,
		GUI.CONTEXT_MENU_SEPARATOR_COLOR,
		GUI.CONTEXT_MENU_BACKGROUND_TRANSPARENCY, 
		GUI.CONTEXT_MENU_SHADOW_TRANSPARENCY
	)

	comboBox.dropDownMenu.onMenuClosed = function(index)
		comboBox.pressed = false
		comboBox.selectedItem = index or comboBox.selectedItem
		comboBox.workspace:draw()
		
		if index and comboBox.onItemSelected then
			comboBox.onItemSelected(index)
		end
	end

	comboBox.selectedItem = 1
	comboBox.addItem = comboBoxAddItem
	comboBox.removeItem = comboBoxRemoveItem
	comboBox.addSeparator = comboBoxAddSeparator
	comboBox.draw = comboBoxDraw
	comboBox.clear = comboBoxClear
	comboBox.getItem = comboBoxGetItem
	comboBox.count = comboBoxCount
	comboBox.eventHandler = comboBoxEventHandler

	return comboBox
end

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

local function windowDraw(window)
	containerDraw(window)

	if window.drawShadow then
		GUI.drawShadow(window.x, window.y, window.width, window.height, GUI.WINDOW_SHADOW_TRANSPARENCY, true)
	end

	return window
end

local function windowCheckForBlockingScreenEvent(object, x, y)
	local child, result

	for i = #object.children, 1, -1 do
		child = object.children[i]
		
		if
			not child.hidden
			and not child.disabled
			and child:isPointInside(x, y)
		then
			if child.blockScreenEvents and child.eventHandler then
				return true

			elseif child.children then
				if windowCheckForBlockingScreenEvent(child, x, y) then
					return true
				end
			end
		end
	end
end

local function windowEventHandler(workspace, window, e1, e2, e3, e4, ...)
	if not window.movingEnabled then
		return
	end

	if e1 == "touch" then
		e3, e4 = math.ceil(e3), math.ceil(e4)

		if not windowCheckForBlockingScreenEvent(window, e3, e4) then
			workspace.capturedObject = window
			window.lastTouchX, window.lastTouchY = e3, e4
		end

		-- Focusing window if it's not focused
		if window ~= window.parent.children[#window.parent.children] then
			window:focus()
			
			workspace:draw()
		end
	
	elseif e1 == "drag" and window.lastTouchX then
		e3, e4 = math.ceil(e3), math.ceil(e4)

		window.localX, window.localY = window.localX + e3 - window.lastTouchX, window.localY + e4 - window.lastTouchY
		window.lastTouchX, window.lastTouchY = e3, e4
		
		workspace:draw()
	
	elseif e1 == "drop" then
		window.lastTouchX, window.lastTouchY, workspace.capturedObject = nil, nil, nil
	end
end

local function windowResize(window, width, height, ignoreOnResizeFinished)
	window.width, window.height = width, height
	
	if window.onResize then
		window.onResize(width, height)
	end

	if window.onResizeFinished and not ignoreOnResizeFinished then
		window.onResizeFinished()
	end

	return window
end

function GUI.windowMaximize(window, animationDisabled)
	local fromX, fromY, fromWidth, fromHeight, toX, toY, toWidth, toHeight = window.localX, window.localY, window.width, window.height
	
	if window.maximized then
		toX, toY, toWidth, toHeight = window.oldGeometryX, window.oldGeometryY, window.oldGeometryWidth, window.oldGeometryHeight

		window.oldGeometryX, window.oldGeometryY, window.oldGeometryWidth, window.oldGeometryHeight = nil, nil, nil, nil
		window.maximized = nil
	else
		toWidth, toHeight = window.parent.width, window.parent.height
		
		if window.maxWidth then
			toWidth = math.min(toWidth, window.maxWidth)
		end
		
		if window.maxHeight then
			toHeight = math.min(toHeight, window.maxHeight)
		end

		toX, toY = math.floor(1 + window.parent.width / 2 - toWidth / 2), math.floor(1 + window.parent.height / 2 - toHeight / 2)

		window.oldGeometryX, window.oldGeometryY, window.oldGeometryWidth, window.oldGeometryHeight = window.localX, window.localY, window.width, window.height
		window.maximized = true
	end

	if animationDisabled then
		window.localX, window.localY = toX, toY
		window:resize(toWidth, toHeight)
	else
		window:addAnimation(
			function(animation)
				window.localX, window.localY =
					math.floor(fromX + (toX - fromX) * animation.position),
					math.floor(fromY + (toY - fromY) * animation.position)

				window:resize(
					math.floor(fromWidth + (toWidth - fromWidth) * animation.position),
					math.floor(fromHeight + (toHeight - fromHeight) * animation.position),
					animation.position < 1
				)
			end,
			function(animation)
				animation:remove()
			end
		):start(0.5)
	end
end

local function windowFocus(window)
	window.workspace.focusedObject = window
	window.hidden = false
	window:moveToFront()

	if window.onFocus then
		window.onFocus()
	end
end

function GUI.windowMinimize(window)
	window.hidden = not window.hidden
end

function GUI.window(x, y, width, height)
	local window = GUI.container(x, y, width, height)
	
	window.blockScreenEvents = true
	window.movingEnabled = true
	window.drawShadow = true

	window.resize = windowResize
	window.maximize = GUI.windowMaximize
	window.minimize = GUI.windowMinimize

	window.eventHandler = windowEventHandler
	window.draw = windowDraw
	window.focus = windowFocus

	return window
end

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

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

	return window
end

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

	if addTitlePanel then
		window.titlePanel = window:addChild(GUI.panel(1, 1, width, 1, GUI.WINDOW_TITLE_BACKGROUND_COLOR))
		window.backgroundPanel.localY, window.backgroundPanel.height = 2, window.height - 1
	end

	window.titleLabel = window:addChild(GUI.label(1, 1, width, height, GUI.WINDOW_TITLE_TEXT_COLOR, title)):setAlignment(GUI.ALIGNMENT_HORIZONTAL_CENTER, GUI.ALIGNMENT_VERTICAL_TOP)
	window.actionButtons.localY = 1
	window.actionButtons:moveToFront()

	return window
end

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

	window.tabBar = window:addChild(GUI.tabBar(1, 1, window.width, 3, 2, 0, GUI.WINDOW_TAB_BAR_DEFAULT_BACKGROUND_COLOR, GUI.WINDOW_TAB_BAR_DEFAULT_TEXT_COLOR, GUI.WINDOW_TAB_BAR_DEFAULT_BACKGROUND_COLOR, GUI.WINDOW_TAB_BAR_DEFAULT_TEXT_COLOR, GUI.WINDOW_TAB_BAR_SELECTED_BACKGROUND_COLOR, GUI.WINDOW_TAB_BAR_SELECTED_TEXT_COLOR, true))
	
	window.backgroundPanel.localY, window.backgroundPanel.height = 4, window.height - 3
	window.actionButtons:moveToFront()
	window.actionButtons.localY = 2

	return window
end

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

function GUI.tabBar(...)
	local tabBar = GUI.list(...)

	tabBar:setDirection(GUI.DIRECTION_HORIZONTAL)
	tabBar:setAlignment(GUI.ALIGNMENT_HORIZONTAL_CENTER, GUI.ALIGNMENT_VERTICAL_TOP)

	return tabBar
end

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

local function menuDraw(menu)
	screen.drawRectangle(menu.x, menu.y, menu.width, 1, menu.colors.default.background, menu.colors.default.text, " ", menu.colors.transparency)
	
	local x = 1

	local child
	for i = 1, #menu.children do
		child = menu.children[i]

		child.localX = x

		x = x + child.width
	end

	containerDraw(menu)
end

local function menuAddItem(menu, text, textColor)
	local item = menu:addChild(pressable(1, 1, unicode.wlen(text) + 2, 1, nil, textColor or menu.colors.default.text, menu.colors.selected.background, menu.colors.selected.text, 0x0, 0x0, text))
	item.eventHandler = pressableEventHandler

	return item
end

local function menuGetItem(menu, what)
	if type(what) == "number" then
		return menu.children[what]
	else
		for i = 1, #menu.children do
			if menu.children[i].text == what then
				return menu.children[i], i
			end
		end
	end
end

local function menuContextMenuItemOnTouch(workspace, item)
	item.contextMenu.x, item.contextMenu.y = item.x, item.y + 1
	dropDownMenuAdd(workspace, item.contextMenu)

	workspace:draw()
end

local function menuAddContextMenuItem(menu, ...)
	local item = menu:addItem(...)

	item.switchMode = true
	item.onTouch = menuContextMenuItemOnTouch
	item.contextMenu = contextMenuCreate(1, 1)
	item.contextMenu.onMenuClosed = function()
		item.pressed = false
		item.workspace:draw()
	end

	return item.contextMenu
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,
		},
		selected = {
			background = backgroundPressedColor,
			text = textPressedColor,
		},
		transparency = backgroundTransparency
	}
	
	menu.blockScreenEvents = true
	menu.addContextMenuItem = menuAddContextMenuItem
	menu.addItem = menuAddItem
	menu.getItem = menuGetItem
	menu.draw = menuDraw

	-- menu:setDirection(1, 1, GUI.DIRECTION_HORIZONTAL)
	-- menu:setAlignment(1, 1, GUI.ALIGNMENT_HORIZONTAL_LEFT, GUI.ALIGNMENT_VERTICAL_TOP)
	-- menu:setSpacing(1, 1, 0)
	-- menu:setMargin(1, 1, 1, 0)

	return menu
end

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

local function progressIndicatorDraw(self)
	local color = self.active and (self.position == 1 and self.colors.secondary or self.colors.primary) or self.colors.passive
	screen.drawText(self.x + 1, self.y, color, "⢀")
	screen.drawText(self.x + 2, self.y, color, "⡀")

	color = self.active and (self.position == 2 and self.colors.secondary or self.colors.primary) or self.colors.passive
	screen.drawText(self.x + 3, self.y + 1, color, "⠆")
	screen.drawText(self.x + 2, self.y + 1, color, "⢈")

	color = self.active and (self.position == 3 and self.colors.secondary or self.colors.primary) or self.colors.passive
	screen.drawText(self.x + 1, self.y + 2, color, "⠈")
	screen.drawText(self.x + 2, self.y + 2, color, "⠁")

	color = self.active and (self.position == 4 and self.colors.secondary or self.colors.primary) or self.colors.passive
	screen.drawText(self.x, self.y + 1, color, "⠰")
	screen.drawText(self.x + 1, self.y + 1, color, "⡁")
end

local function progressIndicatorRoll(self)
	self.position = self.position + 1
	if self.position > 4 then
		self.position = 1
	end
end

local function progressIndicatorReset(self, state)
	self.active = state
	self.position = 1
end

function GUI.progressIndicator(x, y, passiveColor, primaryColor, secondaryColor)
	local object = GUI.object(x, y, 4, 3)
	
	object.colors = {
		passive = passiveColor,
		primary = primaryColor,
		secondary = secondaryColor
	}

	object.active = false
	object.reset = progressIndicatorReset
	object.draw = progressIndicatorDraw
	object.roll = progressIndicatorRoll

	object:reset()

	return object
end

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

local function tableHeaderDraw(self)
	screen.drawRectangle(self.x, self.y, self.width, self.height, self.parent.colors.headerBackground, self.parent.colors.headerForeground, " ")
	screen.drawText(self.x + 1, self.y, self.parent.colors.headerForeground, self.text)
end

local function tableAddColumn(self, headerText, sizePolicy, size)
	layoutAddColumn(self, sizePolicy, size)
	
	local lastColumn = #self.columnSizes
	
	local header = self:setPosition(lastColumn, 1, self:addChild(GUI.object(1, 1, 1, self.itemHeight)))
	header.text = headerText
	header.draw = tableHeaderDraw

	for row = 1, 2 do
		self:setAlignment(lastColumn, row, GUI.ALIGNMENT_HORIZONTAL_LEFT, GUI.ALIGNMENT_VERTICAL_TOP)
		self:setSpacing(lastColumn, row, 0)
		self:setFitting(lastColumn, row, true, false)
	end
end

local function tableAddRow(self, ...)
	local objects, columnCount = {...}, #self.columnSizes
	local index = #self.children - columnCount + 1
	
	if #objects == columnCount then
		for i = #objects, 1, -1 do
			local object = self:setPosition(i, 2, self:addChild(objects[i], index))

			object.height = self.itemHeight
			object.alternative = self.nextRowAlternative
		end

		self.nextRowAlternative = not self.nextRowAlternative
	else
		error("Failed to add row: count of columns ~= count of objects in row")
	end
end

local function tableUpdateSelection(self)
	local columnCount, row = #self.columnSizes, 1
	
	for i = 1, #self.children - columnCount, columnCount do
		for j = i, i + columnCount - 1 do
			self.children[j].selected = self.selectedRows[row]
		end

		row = row + 1
	end
end

local function tableClear(self)
	local columnCount, childrenCount = #self.columnSizes, #self.children
	if childrenCount > columnCount then
		self:removeChildren(1, childrenCount - columnCount)
	end

	self.selectedRows, self.nextRowAlternative = {}, nil
end

function GUI.tableCellEventHandler(workspace, self, e1, e2, e3, e4, e5, ...)
	if e1 == "touch" or e1 == "drag" or e1 == "double_touch" then
		local row = math.ceil(self:indexOf() / #self.parent.columnSizes)

		-- Deselecting all rows
		if (e5 == 0 or not self.parent.selectedRows[row]) and not (keyboard.isControlDown() or keyboard.isCommandDown()) then
			self.parent.selectedRows = {}
		end

		-- Selecting this row
		self.parent.selectedRows[row] = true
		tableUpdateSelection(self.parent)

		if self.parent.onCellTouch then
			self.parent.onCellTouch(workspace, self, e1, e2, e3, e4, e5, ...)
		end

		workspace:draw()
	end
end

function GUI.tableCellDraw(self)
	local background, foreground

	if self.selected then
		background, foreground =
			self.colors.selectionBackground or self.parent.colors.itemSelectionBackground,
			self.colors.selectionForeground or self.parent.colors.itemSelectionForeground
	
	elseif self.alternative then
		background, foreground =
			self.colors.alternativeBackground or self.parent.colors.itemAlternativeBackground,
			self.colors.alternativeForeground or self.parent.colors.itemAlternativeForeground
	
	else
		background, foreground =
			self.colors.defaultBackground or self.parent.colors.itemDefaultBackground,
			self.colors.defaultForeground or self.parent.colors.itemDefaultForeground
	end

	if background then
		screen.drawRectangle(self.x, self.y, self.width, self.height, background, foreground or 0x0, " ")
	end

	return foreground or 0x0
end

function GUI.tableCell()
	local cell = GUI.object(1, 1, 1, 1)

	cell.colors = {}
	cell.draw = GUI.tableCellDraw
	cell.eventHandler = GUI.tableCellEventHandler

	return cell
end

local function tableTextCellDraw(self)
	screen.drawText(self.x + 1, self.y, GUI.tableCellDraw(self) or 0x0, self.text)
end

function GUI.tableTextCell(text)
	local cell = GUI.tableCell()

	cell.text = text
	cell.draw = tableTextCellDraw

	return cell
end

local function tableDraw(self)
	-- Items background
	screen.drawRectangle(self.x, self.y + self.itemHeight, self.width, self.height - self.itemHeight, self.colors.background, 0x0, " ")
	-- Content
	layoutDraw(self)
end

local function tableGetVerticalScroll(self)
	local horizontalMargin, verticalMargin = self:getMargin(1, 2)
	return verticalMargin
end

local function tableSetVerticalScroll(self, value)
	local horizontalMargin = self:getMargin(1, 2)
	local columnCount = #self.columnSizes
	local maxValue = -self.itemHeight * (#self.children - columnCount) / columnCount + 1

	for i = 1, columnCount do
		self:setMargin(i, 2, horizontalMargin,
			math.max(
				maxValue,
				math.min(
					0,
					value
				)
			)
		)
	end
end

function GUI.tableEventHandler(workspace, self, e1, e2, e3, e4, e5, ...)
	if e1 == "touch" then
		local itemTouched = false

		for i = 1, #self.children do
			if self.children[i]:isPointInside(math.ceil(e3), math.ceil(e4)) then
				itemTouched = true
				break
			end
		end

		if not itemTouched then
			if self.onBackgroundTouch then
				self.onBackgroundTouch(workspace, self, e1, e2, e3, e4, e5, ...)
			end
		end

	elseif e1 == "scroll" then
		self:setVerticalScroll(self:getVerticalScroll() + e5)
		workspace:draw()
	end
end

function GUI.table(
	x,
	y,
	width,
	height,
	itemHeight,

	background,
	
	headerBackground,
	headerForeground,

	itemDefaultBackground,
	itemDefaultForeground,

	itemAlternativeBackground,
	itemAlternativeForeground,

	itemSelectionBackground,
	itemSelectionForeground
)
	local table = GUI.layout(x, y, width, height, 0, 2)

	table.colors = {
		background = background,

		headerBackground = headerBackground,
		headerForeground = headerForeground,

		itemDefaultBackground = itemDefaultBackground,
		itemDefaultForeground = itemDefaultForeground,

		itemAlternativeBackground = itemAlternativeBackground,
		itemAlternativeForeground = itemAlternativeForeground,

		itemSelectionBackground = itemSelectionBackground,
		itemSelectionForeground = itemSelectionForeground
	}

	table.blockScreenEvents = true
	table.itemHeight = itemHeight
	table.selectedRows = {}

	table.addColumn = tableAddColumn
	table.addRow = tableAddRow
	table.setVerticalScroll = tableSetVerticalScroll
	table.getVerticalScroll = tableGetVerticalScroll
	table.clear = tableClear
	table.draw = tableDraw
	table.eventHandler = GUI.tableEventHandler

	table:setRowHeight(1, GUI.SIZE_POLICY_ABSOLUTE, itemHeight)
	table:setRowHeight(2, GUI.SIZE_POLICY_RELATIVE, 1.0)

	return table
end

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

return GUI
