local screen = require("Screen")
local GUI = require("GUI")
local color = require("Color")
local filesystem = require("Filesystem")
local system = require("System")
local paths = require("Paths")
local system = require("System")
local text = require("Text")
local event = require("Event")
local number = require("Number")

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

local socketHandle
local socketConnectDelay = 1
local socketReadDelay = 1
local oldUptime = 0

local channelUsersList = {operators = {}, voiced = {}, default = {}}
local channelUsersListUpdatingFinished = true

local userZoneMaxWidth = 26
local chatTimeWidth = 5

local selectedItem

local socketUsername = "Socket "

local systemNames = {
	["NickServ"] = true,
	["ChanServ"] = true,
	["DerpServ"] = true,
	[socketUsername] = true,
}

local colorScheme = {
	noticeMessageSender = 0x0049BF,
	noticeMessageText = 0x0092FF,
	actionMessageSender = 0xCC9200,
	actionMessageText = 0xFFB600,
	myMessageSender = 0x2D2D2D,
	myMessageText = 0x969696,
	otherMessageSender = 0x2D2D2D,
	otherMessageText = 0x696969,
	channelDataMessageSender = 0x696969,
	channelDataMessageText = 0xB4B4B4,
}

local applicationPath = paths.user.applicationData .. "IRC/"
local configPath = applicationPath .. "Config.cfg"
local historyPath = applicationPath .. "History.cfg"

local config = {
	server = "irc.esper.net",
	port = 6667,
	username = "",
	password = "",
	historyLimit = 50,
}

local history = {
	["#cc.ru"] = {},
	["#oc"] = {},
	[socketUsername] = {},
	["NickServ"] = {},
	["ECS"] = {},
}

if filesystem.exists(configPath) then
	config = filesystem.readTable(configPath)
end

if filesystem.exists(historyPath) then
	history = filesystem.readTable(historyPath)
end

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

local workspace, window = system.addWindow(GUI.filledWindow(1, 1, 110, 27, 0xE1E1E1))

local leftPanel = window:addChild(GUI.panel(1, 1, 21, 1, 0x2D2D2D))
local leftLayout = window:addChild(GUI.layout(1, 4, leftPanel.width, 1, 1, 1))
leftLayout:setAlignment(1, 1, GUI.ALIGNMENT_HORIZONTAL_LEFT, GUI.ALIGNMENT_VERTICAL_TOP)
leftLayout:setMargin(1, 1, 0, 0)

local channelsText = leftLayout:addChild(GUI.text(1, 1, 0xE1E1E1, "  Channels"))
local channelsList = leftLayout:addChild(GUI.list(1, 1, leftPanel.width, 1, 1, 0, 0x2D2D2D, 0x696969, 0x2D2D2D, 0x696969, 0x3366CC, 0xE1E1E1, false))

local usersText = leftLayout:addChild(GUI.text(1, 1, 0xE1E1E1, "  Users"))
local usersList = leftLayout:addChild(GUI.list(1, 1, leftPanel.width, 1, 1, 0, 0x2D2D2D, 0x696969, 0x2D2D2D, 0x696969, 0x3366CC, 0xE1E1E1, false))

local systemText = leftLayout:addChild(GUI.text(1, 1, 0xE1E1E1, "  System"))
local systemList = leftLayout:addChild(GUI.list(1, 1, leftPanel.width, 1, 1, 0, 0x2D2D2D, 0x696969, 0x2D2D2D, 0x696969, 0x3366CC, 0xE1E1E1, false))

local contactLayout = window:addChild(GUI.layout(1, 1, leftPanel.width, 3, 1, 1))
contactLayout:setAlignment(1, 1, GUI.ALIGNMENT_HORIZONTAL_CENTER, GUI.ALIGNMENT_VERTICAL_TOP)
contactLayout:setDirection(1, 1, GUI.DIRECTION_HORIZONTAL)
contactLayout:setSpacing(1, 1, 0)

local contactButtonWidth = 7
local contactAddButton = contactLayout:addChild(GUI.button(1, 1, contactButtonWidth, contactLayout.height, 0x3C3C3C, 0xA5A5A5, 0x878787, 0xB4B4B4, "+"))
local contactRemoveButton = contactLayout:addChild(GUI.button(1, 1, contactButtonWidth, contactLayout.height, 0x4B4B4B, 0xA5A5A5, 0x878787, 0xB4B4B4, "-"))
contactRemoveButton.colors.disabled.background = 0x4B4B4B
contactRemoveButton.colors.disabled.text = 0x696969
local settingsButton = contactLayout:addChild(GUI.button(1, 1, contactButtonWidth, contactLayout.height, 0x3C3C3C, 0xA5A5A5, 0x878787, 0xB4B4B4, "*"))

window.backgroundPanel.width = 18
local rightLayout = window:addChild(GUI.layout(1, 1, window.backgroundPanel.width - 2, 1, 1, 1))
rightLayout:setAlignment(1, 1, GUI.ALIGNMENT_HORIZONTAL_LEFT, GUI.ALIGNMENT_VERTICAL_TOP)
rightLayout:setMargin(1, 1, 0, 0)
rightLayout:setSpacing(1, 1, 0)

local chat = window:addChild(GUI.object(1, 1, 1, 1))
chat.blockScreenEvents = false

local scrollBar = window:addChild(GUI.scrollBar(1, 1, 1, 1, 0xD2D2D2, 0x878787, 1, 1, 1, 1, 1, true))

local chatInputLayout = window:addChild(GUI.layout(1, 1, 1, 3, 1, 1))
chatInputLayout:setAlignment(1, 1, GUI.ALIGNMENT_HORIZONTAL_LEFT, GUI.ALIGNMENT_VERTICAL_TOP)
chatInputLayout:setDirection(1, 1, GUI.DIRECTION_HORIZONTAL)
chatInputLayout:setSpacing(1, 1, 0)

local chatInput = chatInputLayout:addChild(GUI.input(1, 1, 36, 3, 0xE1E1E1, 0x696969, 0xA5A5A5, 0xE1E1E1, 0x2D2D2D, "", "Type message here"))
chatInput.historyEnabled = true

local chatComboBox = chatInputLayout:addChild(GUI.comboBox(1, 1, 14, 3, 0xD2D2D2, 0x4B4B4B, 0xD2D2D2, 0x969696))
chatComboBox.dropDownMenu.itemHeight = 1
chatComboBox:addItem("/msg")
chatComboBox:addItem("/me")
chatComboBox:addItem("/notice")
chatComboBox:addItem("/ctcp")
chatComboBox:addItem("/raw")

local backgroundContainer = window:addChild(GUI.container(1, 1, 1, 1))
local backgroundPanel = backgroundContainer:addChild(GUI.panel(1, 1, 1, 1, 0xF0F0F0))
local backgroundLayout = backgroundContainer:addChild(GUI.layout(1, 1, 1, 1, 1, 1))
backgroundLayout.hidden = true

local loginServerLayout = GUI.layout(1, 1, 36, 3, 1, 1)
loginServerLayout:setAlignment(1, 1, GUI.ALIGNMENT_HORIZONTAL_LEFT, GUI.ALIGNMENT_VERTICAL_TOP)
loginServerLayout:setDirection(1, 1, GUI.DIRECTION_HORIZONTAL)

local loginServerInput = loginServerLayout:addChild(GUI.input(1, 1, 27, 3, 0xE1E1E1, 0x787878, 0xB4B4B4, 0xE1E1E1, 0x2D2D2D, config.server or "", "Server"))
local loginPortInput = loginServerLayout:addChild(GUI.input(1, 1, loginServerLayout.width - loginServerInput.width - 1, 3, 0xE1E1E1, 0x787878, 0xB4B4B4, 0xE1E1E1, 0x2D2D2D, config.port and tostring(config.port) or "", "Port"))

local loginUsernameInput = GUI.input(1, 1, 36, 3, 0xE1E1E1, 0x787878, 0xB4B4B4, 0xE1E1E1, 0x2D2D2D, config.username or "", "Username")
local loginPasswordInput = GUI.input(1, 1, 36, 3, 0xE1E1E1, 0x787878, 0xB4B4B4, 0xE1E1E1, 0x2D2D2D, config.password or "", "Password")
local loginPasswordSwitchAndLabel = GUI.switchAndLabel(1, 1, 36, 8, 0x66DB80, 0xE1E1E1, 0xFFFFFF, 0xB4B4B4, "Log into NickServ:", false)
local loginSubmitButton = GUI.button(1, 1, 36, 3, 0xC3C3C3, 0xFFFFFF, 0x969696, 0xFFFFFF, "Connect")
local loginStatusText = GUI.text(1, 1, 0xA5A5A5, "")

window.actionButtons.localX = 3
window.actionButtons:moveToFront()

local function stringHash(string)
	local hash = 0
	for i = 1, string.len (string) do
		hash = hash + string.byte (string:sub (i, i))
	end

	return hash
end

local function generateUniqueColor(string)
	local safeColors = {
		0x9900FF,
		0x990040,
		0xCC0000,
		0xFF6D00,
		0x33DB00,
		0x33DB80,
		0x99B6B6,
		0x006D80,
		0x0092FF,
		0x6692BF,
		0x002440,
		0x3349FF,
		0x666DBF,
		0x6624FF,
		0x3399BF,
		0xCC24FF,
		0x660080,
		0xCC0080
	}

	math.randomseed (stringHash (tostring (string)))
	return safeColors[math.random (9999999) % #safeColors + 1]
end

local function getProperList(name)
	return systemNames[name] and systemList or name:sub(1, 1) == "#" and channelsList or usersList
end

local function getItemByText(name)
	return getProperList(name):getItem(name)
end

local function saveConfig()
	filesystem.writeTable(configPath, config)
end

local function socketWrite(data)
	local success, result = pcall(socketHandle.write, data .. "\r\n")
	if success then
		return true, result
	else
		return false, result
	end
end

local function sendMessage(target, text, notice, ctcp)
	socketWrite((notice and "NOTICE " or "PRIVMSG ") .. target .. (ctcp and " :\x01" or " :") .. text .. (ctcp and "\x01" or ""))
end

local function status(text)
	local state = text ~= nil
	if text then
		loginStatusText.text = text
		loginStatusText:update()
	end

	loginStatusText.hidden, loginServerLayout.hidden, loginUsernameInput.hidden, loginPasswordSwitchAndLabel.hidden, loginSubmitButton.hidden = not state, state, state, state, state
	loginPasswordInput.hidden = state and true or not loginPasswordSwitchAndLabel.switch.state

	workspace:draw()
end

local function updateLeftLayout()
	local function checkTextAndList(text, list)
		text.hidden = #list.children == 0
		list.hidden = text.hidden
		list.height = #list.children
	end

	checkTextAndList(channelsText, channelsList)
	checkTextAndList(usersText, usersList)
	checkTextAndList(systemText, systemList)
end

local function contactItemDraw(item)
	local background = item.pressed and item.colors.pressed.background or item.colors.default.background
	local foreground = item.pressed and item.colors.pressed.text or item.colors.default.text

	screen.drawRectangle(item.x, item.y, item.width, item.height, background, foreground, " ")

	local y, textLimit = math.floor(item.y + item.height / 2)
	if not item.pressed and item.unreadCount > 0 then
		local tipString = tostring(item.unreadCount)
		local tipWidth = #tipString + 2
		
		screen.drawRectangle(item.x + item.width - tipWidth - 1, y, tipWidth, 1, 0x3C3C3C, 0xE1E1E1, " ")
		screen.drawText(item.x + item.width - tipWidth, y, 0xE1E1E1, tipString)

		textLimit = item.width - 4 - tipWidth
	else
		textLimit = item.width - 4
	end

	screen.drawText(item.x + 2, y, foreground, text.limit(item.text, textLimit, "right"))
end

local function addContactItemToList(name)
	if not history[name] then
		history[name] = {}
	end

	local list = getProperList(name)
	local item = list:addItem(name)

	item.unreadCount = 0
	item.draw = contactItemDraw

	item.onTouch = function()
		if list == channelsList then
			usersList.selectedItem = nil
			systemList.selectedItem = nil
		elseif list == usersList then
			channelsList.selectedItem = nil
			systemList.selectedItem = nil
		else
			channelsList.selectedItem = nil
			usersList.selectedItem = nil
		end

		selectedItem = item
		item.unreadCount = 0
		contactRemoveButton.disabled = systemNames[item.text]
		rightLayout.cells[1][1].verticalMargin = 0

		scrollBar.maximumValue = #history[name]
		scrollBar.value = scrollBar.maximumValue
		
		rightLayout:removeChildren()
		workspace:draw()

		if socketHandle then
			if list == channelsList then
				if item.joined then
					socketWrite("NAMES " .. name)
				else
					item.joined = true
					socketWrite("JOIN " .. name)
				end
			end
		end
	end

	table.sort(list.children, function(a, b) return a.text:lower() < b.text:lower() end)
	updateLeftLayout()

	return item
end

local function addChatMessage(conversationName, text, sender)
	if not history[conversationName] then
		addContactItemToList(conversationName)
	end

	text = text:gsub("[\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F]+", "")
	
	local chars = {}
	for i = 1, unicode.len(text) do
		chars[i] = unicode.sub(text, i, i)
		if unicode.isWide(chars[i]) then
			chars[i] = "?"
		end
	end
	text = table.concat(chars)

	local message = {
		text = text,
		time = os.date("%H:%M", system.getTime()),
		sender = sender,
		color = generateUniqueColor(sender)
	}

	table.insert(history[conversationName], message)
	if #history[conversationName] > config.historyLimit then
		table.remove(history[conversationName], 1)
	end

	local item = getItemByText(conversationName)
	if item == selectedItem then
		scrollBar.maximumValue = #history[conversationName]
		
		if scrollBar.value == scrollBar.maximumValue - 1 then
			scrollBar.value = scrollBar.maximumValue
		end
	elseif item.unreadCount < config.historyLimit then
		item.unreadCount = item.unreadCount + 1
	end

	return message
end

chat.draw = function()
	screen.drawRectangle(chat.x, chat.y, chat.width, chat.height, 0xF0F0F0, colorScheme.otherMessageText, " ")

	if history[selectedItem.text] and #history[selectedItem.text] > 0 then
		local y, userZoneWidth, messages = chat.y + chat.height - 2, 0, history[selectedItem.text]

		for i = scrollBar.value, 1, -1 do
			userZoneWidth = math.max(userZoneWidth, unicode.len(messages[i].sender or selectedItem.text))
		end
		userZoneWidth = math.min(userZoneWidth + chatTimeWidth + 4, userZoneMaxWidth)

		for i = chat.y, chat.y + chat.height - 1 do
			local index = screen.getIndex(chat.x + userZoneWidth - 1, i)
			local background, foreground, symbol = screen.rawGet(index)

			screen.rawSet(index, background, 0xE1E1E1, "│")
		end
		
		for i = scrollBar.value, 1, -1 do
			if messages[i].newMessages then
				local elda = " New messages "
				local pizda = chat.width - userZoneWidth - 3 - unicode.len(elda)
				local klitor = math.floor(pizda / 2)
				local vagina = pizda - klitor

				screen.drawText(chat.x + userZoneWidth + 1, y, 0xFF9280, string.rep("─", klitor) .. elda .. string.rep("─", vagina))
				y = y - 1
			else
				local wrappedLines = text.wrap(messages[i].text, chat.width - userZoneWidth - 3)

				local senderColor, textColor
				if messages[i].notice then
					senderColor, textColor = colorScheme.noticeMessageSender, colorScheme.noticeMessageText
				elseif messages[i].action then
					senderColor, textColor = colorScheme.actionMessageSender, colorScheme.actionMessageText
				elseif messages[i].channelAction then
					senderColor, textColor = colorScheme.channelDataMessageSender, colorScheme.channelDataMessageText
				else
					if not messages[i].color then -- В исходной версии приложухи, в History.cfg цвет не прописан
						messages[i].color = generateUniqueColor (messages[i].sender)
					end
					senderColor = messages[i].color

					if messages[i].sender == config.username then
						textColor = colorScheme.myMessageText
					else
						textColor = colorScheme.otherMessageText
					end
				end

				for j = #wrappedLines, 1, -1 do
					screen.drawText(chat.x + userZoneWidth + 1, y, textColor, wrappedLines[j])

					y = y - 1
					if y < chat.y then
						return
					end
				end

				local sender = messages[i].sender or selectedItem.text
				local limited = text.limit(sender, userZoneWidth - chatTimeWidth - 4, "center")
				screen.drawText(chat.x + userZoneWidth - unicode.len(limited) - 2, y + 1, senderColor, limited)
				screen.drawText(chat.x + 1, y + 1, 0xC3C3C3, messages[i].time)
			end

			y = y - 1
		end
	end
end

local function checkLoginInputs()
	loginSubmitButton.disabled = #loginUsernameInput.text == 0 or #loginServerInput.text == 0 or not loginPortInput.text:match("^%d+$")
end

loginUsernameInput.onInputFinished = function()
	checkLoginInputs()
	workspace:draw()
end

loginPasswordInput.onInputFinished = loginUsernameInput.onInputFinished
loginServerInput.onInputFinished = loginUsernameInput.onInputFinished
loginPortInput.onInputFinished = loginUsernameInput.onInputFinished

local function login()
	backgroundLayout.hidden = false

	backgroundLayout:removeChildren()
	backgroundLayout:addChild(loginServerLayout)
	backgroundLayout:addChild(loginUsernameInput)
	backgroundLayout:addChild(loginPasswordInput)
	backgroundLayout:addChild(loginPasswordSwitchAndLabel)
	backgroundLayout:addChild(loginSubmitButton)
	backgroundLayout:addChild(loginStatusText)

	checkLoginInputs()
	status()
end

chatInput.onInputFinished = function()
	if #chatInput.text > 0 then
		if chatComboBox.selectedItem == 1 then			
			sendMessage(selectedItem.text, chatInput.text)
			addChatMessage(selectedItem.text, chatInput.text, config.username)
		elseif chatComboBox.selectedItem == 2 then			
			sendMessage(selectedItem.text, "ACTION " .. chatInput.text, false, true)
			addChatMessage(selectedItem.text, chatInput.text, config.username, "!").action = true
		elseif chatComboBox.selectedItem == 3 then
			sendMessage(selectedItem.text, chatInput.text, true)
			addChatMessage(selectedItem.text, chatInput.text, config.username, "!").notice = true
		elseif chatComboBox.selectedItem == 3 then
			sendMessage(selectedItem.text, chatInput.text, false, true)
			addChatMessage(selectedItem.text, chatInput.text, config.username, "!").action = true
		else
			socketWrite(chatInput.text)
			addChatMessage(selectedItem.text, chatInput.text, config.username)
		end

		chatInput.text = ""

		workspace:draw()
	end
end

local function addScrollEventHandler(layout)
	local function getTotalHeight(layout)
		local height = 0

		for i = 1, #layout.children do
			height = height + layout.children[i].height + (i < #layout.children and layout.cells[1][1].spacing or 0)
		end

		return height
	end

	layout.eventHandler = function(workspace, layout, e1, e2, e3, e4, e5)
		if e1 == "scroll" then
			if e5 > 0 then
				if layout.cells[1][1].verticalMargin < 0 then
					layout.cells[1][1].verticalMargin = layout.cells[1][1].verticalMargin + 1
					workspace:draw()
				end
			else
				if layout.cells[1][1].verticalMargin > -getTotalHeight(layout) + 1 then
					layout.cells[1][1].verticalMargin = layout.cells[1][1].verticalMargin - 1
					workspace:draw()
				end
			end
		end
	end
end

addScrollEventHandler(leftLayout)
addScrollEventHandler(rightLayout)

local function selectItem(item)
	selectedItem = item
	item.parent.selectedItem = item:indexOf()
	item.onTouch()
end

local function userButtonOnTouch(workspace, object)
	local text = object.text:gsub("^[@+]", "")
	if history[text] then
		selectItem(getItemByText(text))
	else
		selectItem(addContactItemToList(text))
	end
end

local function updateUsersLayoutFromList()
	local function addCategory(field, name)
		if #channelUsersList[field] > 0 then
			rightLayout:addChild(GUI.object(1, 1, 1, 1))
			rightLayout:addChild(GUI.text(1, 1, 0x5A5A5A, name)).height = 2

			table.sort(channelUsersList[field], function(a, b) return unicode.lower(a) < unicode.lower(b) end)

			for i = 1, #channelUsersList[field] do
				rightLayout:addChild(GUI.adaptiveButton(1, 1, 0, 0, nil, 0x969696, nil, 0x2D2D2D, channelUsersList[field][i])).onTouch = userButtonOnTouch
			end
		end
	end

	rightLayout:removeChildren()
	addCategory("operators", "Operators")
	addCategory("voiced", "Voiced")
	addCategory("default", "Users")
end

local function addUserToList(name)
	local firstSymbol = name:sub(1, 1)
	local field = firstSymbol == "@" and "operators" or firstSymbol == "+" and "voiced" or "default"

	for i = 1, #channelUsersList[field] do
		if channelUsersList[field][i] == name then
			return true
		end
	end

	table.insert(channelUsersList[field], name)
end

local function removeUserFromList(name)
	for field in pairs(channelUsersList) do
		for i = 1, #channelUsersList[field] do
			if channelUsersList[field][i] == name then
				table.remove(channelUsersList[field], i)
				updateUsersLayoutFromList()
				return true
			end
		end
	end
end

chat.eventHandler = function(workspace, chat, e1, e2, e3, e4, e5)
	if e1 == "scroll" then
		if e5 > 0 then
			if scrollBar.value > 1 then
				scrollBar.value = scrollBar.value - 1
				workspace:draw()
			end
		else
			if scrollBar.value < scrollBar.maximumValue then
				scrollBar.value = scrollBar.value + 1
				workspace:draw()
			end
		end
	elseif not e1 and socketHandle and computer.uptime() - oldUptime > socketReadDelay then
		local data = ""
		while true do
			local success, result = pcall(socketHandle.read, math.huge)
			if success then
				if result and #result > 0 then
					data = data .. result
				elseif #data > 0 then
					for line in data:gmatch("[^\r\n]+") do
						addChatMessage(socketUsername, line)

						local lineWithoutDots = line:match("^:(.+)")
						if lineWithoutDots then
							local words = {}
							for word in lineWithoutDots:gmatch("[^%s]+") do
								table.insert(words, word)
							end

							if #words >= 2 then
								local beforeDots, afterDots = lineWithoutDots:match("^([^:]+):(.+)")
								local username, address = words[1]:match("([^!]+)!([^!]+)")

								if words[2] == "NOTICE" then
									if username then
										if username == "NickServ" and afterDots then
											if afterDots:match("You are now identified") then
												backgroundContainer.hidden = true
											elseif afterDots:match("Invalid password for") then
												GUI.alert("Invalid password")
											end
										end

										addChatMessage(selectedItem.text, afterDots, username).notice = true
									else
										if afterDots and afterDots:match("Looking up your hostname...") then
											socketWrite("USER " .. config.username .. " 0 * :" .. config.username .. "\r\nNICK " .. config.username)
										end
									end
								elseif words[2] == "PRIVMSG" and username then
									local conversationName = words[3]:sub(1, 1) == "#" and words[3] or username

									local ctcp = afterDots:match("^\1(.+)\1$")
									if ctcp then
										local command, data = ctcp:match("^([^%s]+)%s(.+)")
										if ctcp == "VERSION" then
											sendMessage(username, "VERSION MineOS IRC Client / OpenComputers (Lua 5.3)", true, true)
										elseif ctcp == "TIME" then
											sendMessage(username, "TIME " .. os.date(system.getTime()), true, true)
										elseif command == "PING" then
											sendMessage(username, "PING " .. data, true, true)
										elseif command == "ACTION" then
											addChatMessage(conversationName, username .. " " .. data, "*").action = true
										else
											message = "Unsupported CTCP command: " .. ctcp
										end
									-- Regular message
									else
										addChatMessage(conversationName, afterDots, username)
									end

									if config.soundNotifications then
										computer.beep(2000, 0.05)
									end
								elseif words[2] == "JOIN" and username then
									if selectedItem.text == words[3] then
										if not addUserToList(username) then
											updateUsersLayoutFromList()
										end
									end

									addChatMessage(words[3], username .. " (" .. address .. ") has joined the channel", "!").channelAction = true
								elseif words[2] == "NICK" and username then
									addChatMessage(selectedItem.text, username .. " is now known as " .. afterDots, "!").channelAction = true
									local item = getItemByText(username)
									if item then
										history[item.text] = afterDots
										item.text = afterDots
									end
								elseif words[2] == "MODE" and username then
									addChatMessage(words[3], username .. " sets " .. words[5] .. " mode to " .. words[4], "!").channelAction = true
								elseif (words[2] == "QUIT" or words[2] == "PART") and username then
									if removeUserFromList(username) and words[2] == "QUIT" then
										addChatMessage(selectedItem.text, username .. " (" .. address .. ") has quit (" .. afterDots .. ")", "!").channelAction = true
									end

									if words[2] == "PART" and username ~= config.username  then
										addChatMessage(words[3], username .. " (" .. address .. ") has left the channel", "!").channelAction = true
									end
								-- User list
								elseif words[2] == "353" then
									if channelUsersListUpdatingFinished then
										channelUsersList.operators, channelUsersList.voiced, channelUsersList.default = {}, {}, {}
										channelUsersListUpdatingFinished = false
									end

									for username in afterDots:gmatch("[^%s]+") do
										addUserToList(username)
									end									
								-- User list finish
								elseif words[2] == "366" then
									channelUsersListUpdatingFinished = true
									updateUsersLayoutFromList()
								-- No password authorization
								elseif words[2] == "001" then
									if loginPasswordSwitchAndLabel.switch.state then
										status("Waiting for login request...")
									else
										backgroundContainer.hidden = true
									end
								-- Nickname in use
								elseif words[2] == "433" then
									status()
									GUI.alert("Nickname is already in use")
								-- Channel topic
								elseif words[2] == "332" then
									addChatMessage(words[4], afterDots, ">").channelAction = true
								-- Wait a while
								elseif words[2] == "263" then
									addChatMessage(selectedItem.text, afterDots, "!").channelAction = true
								-- End of MOTD
								elseif words[2] == "376" then
									if loginPasswordSwitchAndLabel.switch.state then
										status("Logging in...")
										socketWrite("IDENTIFY " .. config.username .. " " .. config.password)
									end
								-- No such nick/channel
								elseif words[2] == "401" then
									addChatMessage(words[4], "There's no " .. (words[4]:sub(1, 1) == "#" and "channel with such name" or "user with such nick") .. " online", "!").channelAction = true
								end
							end
						else
							local ping = line:match("^PING( :.+)")
							if ping then
								socketWrite("PONG" .. ping)
							end
						end
					end

					workspace:draw()
					break
				else
					break
				end
			else
				GUI.alert("Failed to read data from socket: " .. tostring(result))
				break
			end
		end

		oldUptime = computer.uptime()
	end
end

loginSubmitButton.onTouch = function()
	config.server = loginServerInput.text
	config.port = tonumber(loginPortInput.text)
	config.username = loginUsernameInput.text
	config.password = loginPasswordInput.text
	saveConfig()

	status("Connecting to server socket...")
	
	local result, reason = component.get("internet").connect(config.server, config.port)
	if result then
		socketHandle = result
		repeat
			event.sleep(socketConnectDelay)
		until socketHandle.finishConnect()

		status("Connection estabilished, waiting for response...")
		socketWrite("")
	else
		GUI.alert("Failed to connect to server: " .. tostring(socketHandle))
	end
end

settingsButton.onTouch = function()
	backgroundContainer.hidden = false
	backgroundLayout:removeChildren()
	
	local slider = backgroundLayout:addChild(GUI.slider(1, 1, 36, 0x66DB80, 0xE1E1E1, 0xFFFFFF, 0xB4B4B4, 0, 500, config.historyLimit, false, "History limit: ", ""))
	slider.height = 2
	slider.roundValues = true

	local switchAndLabel = backgroundLayout:addChild(GUI.switchAndLabel(1, 1, 36, 8, 0x66DB80, 0xE1E1E1, 0xFFFFFF, 0xB4B4B4, "Sound notifications:", config.soundNotifications))
	
	backgroundLayout:addChild(GUI.button(1, 1, 36, 3, 0xC3C3C3, 0xFFFFFF, 0x969696, 0xFFFFFF, "Save")).onTouch = function()
		config.historyLimit = math.floor(slider.value)
		config.soundNotifications = switchAndLabel.switch.state
		backgroundContainer.hidden = true
		
		workspace:draw()
		saveConfig()
	end

	workspace:draw()
end

contactAddButton.onTouch = function()
	backgroundContainer.hidden = false
	backgroundLayout:removeChildren()

	local input = backgroundLayout:addChild(GUI.input(1, 1, 36, 3, 0xE1E1E1, 0x787878, 0xB4B4B4, 0xE1E1E1, 0x2D2D2D, "", "User or chat name"))
	backgroundLayout:addChild(GUI.button(1, 1, 36, 3, 0xB4B4B4, 0xFFFFFF, 0x969696, 0xB4B4B4, "Add contact")).onTouch = function()
		backgroundContainer.hidden = true

		if #input.text > 0 and not history[input.text] then
			selectItem(addContactItemToList(input.text))
		else
			workspace:draw()
		end
	end

	workspace:draw()
end

contactRemoveButton.onTouch = function()
	history[selectedItem.text] = nil
	selectedItem:remove()

	if selectedItem.joined then
		socketWrite("PART " .. selectedItem.text)
	end

	selectItem(systemList:getItem(socketUsername))
	updateLeftLayout()

	workspace:draw()
end

loginPasswordSwitchAndLabel.switch.onStateChanged = function()
	status()
end

window.onResize = function(width, height)
	backgroundContainer.width, backgroundContainer.height = width, height
	backgroundPanel.width, backgroundPanel.height = backgroundContainer.width, backgroundContainer.height
	backgroundLayout.width, backgroundLayout.height = backgroundContainer.width, backgroundContainer.height
	chat.localX, chat.width, chat.height = leftLayout.width + 1, width - leftLayout.width - window.backgroundPanel.width, height - chatInput.height
	window.backgroundPanel.localX, window.backgroundPanel.height = width - window.backgroundPanel.width + 1, height
	rightLayout.localX, rightLayout.height = window.backgroundPanel.localX + 1, window.backgroundPanel.height
	leftPanel.height = height - contactLayout.height
	leftLayout.height = leftPanel.height
	contactLayout.localY = height - contactLayout.height + 1
	scrollBar.localX, scrollBar.height = chat.localX + chat.width - 1, chat.height

	chatInputLayout.localX, chatInputLayout.localY, chatInputLayout.width = chat.localX, height - chatInputLayout.height + 1, chat.width
	chatInput.width = chatInputLayout.width - chatComboBox.width
end

scrollBar.onTouch = function()
	scrollBar.value = number.round(scrollBar.value)
	if scrollBar.value < 1 then
		scrollBar.value = 1
	elseif scrollBar.value > #history[selectedItem.text] then
		scrollBar.value = #history[selectedItem.text]
	end

	workspace:draw()
end

local overrideWindowRemove = window.remove
window.remove = function(...)
	if socketHandle then
		socketWrite("QUIT")
	end
	
	overrideWindowRemove(...)

	for key in pairs(history) do
		local i = 1
		while i <= #history[key] do
			if history[key][i].text then
				history[key][i].text = history[key][i].text:gsub("\\", "\\\\")
				history[key][i].text = history[key][i].text:gsub("\"", "\\\"")
				history[key][i].text = history[key][i].text:gsub("\'", "\\\'")
			end

			if history[key][i].newMessages then
				table.remove(history[key], i)
			else
				i = i + 1
			end
		end

		if #history[key] > 0 then
			table.insert(history[key], {newMessages = true})
		end
	end
	
	filesystem.writeTable(historyPath, history)
end

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

for key in pairs(history) do
	addContactItemToList(key)
end

window:resize(window.width, window.height)

selectItem(systemList:getItem(socketUsername))
login()
-- backgroundContainer.hidden = true