
local text = require("Text")
local filesystem = require("Filesystem")
local GUI = require("GUI")
local screen = require("Screen")
local system = require("System")
local paths = require("Paths")

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

local configPath = paths.user.applicationData .. "HEX Editor/Config.cfg"
local config = filesystem.exists(configPath) and filesystem.readTable(configPath) or {
	recentPath = "/OS.lua"
}

local colors = {
	background = 0xF0F0F0,
	backgroundText = 0x5A5A5A,
	panel = 0x1E1E1E,
	panelText = 0x5A5A5A,
	panelSelection = 0x3C3C3C,
	panelSelectionText = 0xE1E1E1,
	selectionFrom = 0x990000,
	selectionTo = 0x990000,
	selectionText = 0xE1E1E1,
	selectionBetween = 0xD2D2D2,
	selectionBetweenText = 0x000000,
	separator = 0xE1E1E1,
	title = 0x2D2D2D,
	titleBackground = 0x990000,
	titleText = 0xE1E1E1
}

local bytes = {}
local offset = 0
local selection = {
	from = 1,
	to = 1,
}

local titles = {"", "", ""}

local scrollBar

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

local workspace, window = system.addWindow(GUI.filledWindow(1, 1, 98, 25, colors.background))

window.maxWidth = window.width
window.showDesktopOnMaximize = true

window.backgroundPanel.localX, window.backgroundPanel.localY = 11, 5
window.backgroundPanel.width = window.width - 10

window.actionButtons.localY = 2

local function byteArrayToNumber(b)
	local n = 0
	for i = 1, #b do
		n = bit32.bor(bit32.lshift(n, 8), b[i])
	end

	return n
end

local function updateTitles()
	titles[1] = "Byte" .. (selection.from == selection.to and "" or "s") .. ": " .. selection.from .. "-" .. selection.to
	titles[2] = "UTF-8: \"" .. string.char(table.unpack(bytes, selection.from, selection.to)) .. "\""
	titles[3] = "Int: " .. byteArrayToNumber({table.unpack(bytes, selection.from, selection.to)})
end

local function byteFieldDraw(object)
	local x, y, index = object.x, object.y, 1 + offset
	local xCount, yCount = math.ceil(object.width / object.elementWidth), math.ceil(object.height / object.elementHeight)
	
	for j = 1, yCount do
		for i = 1, xCount do
			if bytes[index] then
				local textColor = colors.backgroundText
				if index == selection.from or index == selection.to then
					screen.drawRectangle(x - object.offset, y, object.elementWidth, 1, index == selection.from and colors.selectionFrom or colors.selectionTo, colors.selectionText, " ")
					textColor = colors.selectionText
				elseif index > selection.from and index < selection.to then
					screen.drawRectangle(x - object.offset, y, object.elementWidth, 1, colors.selectionBetween, colors.selectionText, " ")
					textColor = colors.selectionBetweenText
				end

				if object.asChar then
					local char = string.char(bytes[index])
					screen.drawText(x, y, textColor, unicode.isWide(char) and "-" or char)
				else
					screen.drawText(x, y, textColor, string.format("%02X", bytes[index]))
				end
			else
				return object
			end

			x, index = x + object.elementWidth, index + 1
		end

		local lastLineIndex = index - 1
		if lastLineIndex >= selection.from and lastLineIndex < selection.to then
			screen.drawRectangle(object.x - object.offset, y + 1, object.width, 1, colors.selectionBetween, colors.selectionText, " ")
		end

		x, y = object.x, y + object.elementHeight
	end

	return object
end

local function byteFieldEventHandler(workspace, object, e1, e2, e3, e4, e5)
	if e1 == "touch" or e1 == "drag" then
		if e5 == 1 then
			local menu = GUI.addContextMenu(workspace, math.ceil(e3), math.ceil(e4))
			
			menu:addItem("✓", "Select all").onTouch = function()
				selection.from = 1
				selection.to = #bytes
				
				workspace:draw()
			end
			
			menu:addSeparator()
			
			menu:addItem("🖊", "Edit").onTouch = function()
				local container = GUI.addBackgroundContainer(workspace, true, true, "Fill byte range [" .. selection.from .. "; " .. selection.to .. "]")

				local input = container.layout:addChild(GUI.input(1, 1, 36, 3, 0xE1E1E1, 0x666666, 0x666666, 0xE1E1E1, 0x2D2D2D, string.format("%02X" , bytes[selection.from]), "Type byte value"))
				
				input.onInputFinished = function(text)
					local number = tonumber("0x" .. input.text)
					if number and number >= 0 and number <= 255 then
						for i = selection.from, selection.to do
							bytes[i] = number
						end

						container:remove()
						workspace:draw()
					end
				end
				
				workspace:draw()
			end
			
			menu:addItem("⇲", "Insert").onTouch = function()
				local container = GUI.addBackgroundContainer(workspace, true, true, "Insert bytes at position " .. selection.from .. "")

				local input = container.layout:addChild(GUI.input(1, 1, 36, 3, 0xE1E1E1, 0x666666, 0x666666, 0xE1E1E1, 0x2D2D2D, "", "Type byte values separated by space", true))
				local switch = container.layout:addChild(GUI.switchAndLabel(1, 1, 36, 8, 0x66DB80, 0x1E1E1E, 0xE1E1E1, 0xBBBBBB, "Select inserted bytes:", true)).switch

				input.onInputFinished = function()
					if input.text:match("[a-fA-F%d%s]+") then
						local insertionPosition, count = selection.from, 0
						for word in input.text:gmatch("[^%s]+") do
							local number = tonumber("0x" .. word)
							if number > 255 then number = 255 end
							table.insert(bytes, insertionPosition + count, number)
							selection.from, selection.to, count = selection.from + 1, selection.to + 1, count + 1
						end

						if switch.state then
							selection.from, selection.to = insertionPosition, insertionPosition + count - 1
						end

						container:remove()
						workspace:draw()
					end
				end
				
				workspace:draw()
			end
			
			menu:addSeparator()
			
			menu:addItem("🗑", "Delete").onTouch = function()
				for i = selection.from, selection.to do
					table.remove(bytes, selection.from)
				end
				if #bytes == 0 then
					selection.from, selection.to = 1, 1
				else
					selection.to = selection.from
				end
			end

			workspace:draw()
		else
			local index = (math.ceil((e4 - object.y + 1) / 2) - 1) * 16 + math.ceil((e3 - object.x + 1 + object.offset) / object.elementWidth) + offset
			
			if bytes[index] then
				if e1 == "touch" then
					selection.to = index
					selection.from = index
					selection.touchIndex = index
				else
					if not selection.touchIndex then selection.touchIndex = index end
					
					if index < selection.touchIndex then
						selection.from = index
						selection.to = selection.touchIndex
					elseif index > selection.touchIndex then
						selection.to = index
						selection.from = selection.touchIndex
					end
				end

				updateTitles()
				workspace:draw()
			end
		end

	elseif e1 == "scroll" then
		offset = offset - 16 * e5
		
		if offset < 0 then
			offset = 0
		elseif offset > math.floor(#bytes / 16) * 16 then
			offset = math.floor(#bytes / 16) * 16
		end

		scrollBar.value = offset

		workspace:draw()
	end
end

local function newByteField(x, y, width, elementWidth, elementHeight, asChar)
	local object = GUI.object(x, y, width, 1)
	
	object.elementWidth = elementWidth
	object.elementHeight = elementHeight
	object.offset = asChar and 0 or 1
	object.asChar = asChar
	object.draw = byteFieldDraw
	object.eventHandler = byteFieldEventHandler

	return object
end

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

window:addChild(GUI.panel(1, 1, window.width, 3, colors.title)):moveToBack()

local byteField = window:addChild(newByteField(13, 6, 64, 4, 2, false))
local charField = window:addChild(newByteField(byteField.localX + byteField.width + 3, 6, 16, 1, 2, true))
local separator = window:addChild(GUI.object(byteField.localX + byteField.width, 5, 1, 1))
separator.draw = function(object)
	for i = object.y, object.y + object.height - 1 do
		screen.drawText(object.x, i, colors.separator, "│")
	end
end

window:addChild(GUI.panel(11, 4, window.width - 10, 1, colors.panel))

-- Vertical
local verticalCounter = window:addChild(GUI.object(1, 4, 10, 1))
verticalCounter.draw = function(object)
	screen.drawRectangle(object.x, object.y, object.width, object.height, colors.panel, colors.panelText, " ")

	local index = offset
	for y = 2, object.height - 1, 2 do
		local textColor = colors.panelText

		if index > selection.from and index < selection.to then
			screen.drawRectangle(object.x, object.y + y - 1, object.width, 2, colors.panelSelection, colors.panelSelectionText, " ")
			textColor = colors.panelSelectionText
		end

		if selection.from >= index and selection.from <= index + 15 or selection.to >= index and selection.to <= index + 15 then
			screen.drawRectangle(object.x, object.y + y, object.width, 1, colors.selectionFrom, colors.selectionText, " ")
			textColor = colors.selectionText
		end

		screen.drawText(object.x + 1, object.y + y, textColor, string.format("%08X", index))

		index = index + 16
	end
end

-- Horizontal
window:addChild(GUI.object(13, 4, 62, 1)).draw = function(object)
	local counter = 0
	local restFrom, restTo = selection.from % 16, selection.to % 16
	for x = 1, object.width, 4 do
		local textColor = colors.panelText
		if counter + 1 > restFrom and counter + 1 < restTo then
			screen.drawRectangle(object.x + x - 2, object.y, 4, 1, colors.panelSelection, colors.selectionText, " ")
			textColor = colors.panelSelectionText
		elseif restFrom == counter + 1 or restTo == counter + 1 then
			screen.drawRectangle(object.x + x - 2, object.y, 4, 1, colors.selectionFrom, colors.selectionText, " ")
			textColor = colors.selectionText
		end

		screen.drawText(object.x + x - 1, object.y, textColor, string.format("%02X", counter))
		counter = counter + 1
	end
end

scrollBar = window:addChild(GUI.scrollBar(window.width, 5, 1, 1, 0xC3C3C3, 0x393939, 0, 1, 1, 160, 1, true))
scrollBar.onTouch = function()
	offset = math.floor(scrollBar.value / 16) * 16
	
	workspace:draw()
end

local titleTextBoxWidth = window.width * 0.35
titleTextBox = window:addChild(GUI.object(math.floor(window.width / 2 - titleTextBoxWidth / 2), 1, math.floor(titleTextBoxWidth), #titles))
titleTextBox.draw = function(titleTextBox)
	screen.drawRectangle(titleTextBox.x, titleTextBox.y, titleTextBox.width, titleTextBox.height, colors.titleBackground, colors.titleText, " ")
	
	local title

	for y = 1, #titles do
		title = titles[y]
		title = #title < titleTextBox.width - 2 and title or unicode.wtrunc(title, titleTextBox.width - 2)

		screen.drawText(math.floor(titleTextBox.x + titleTextBox.width / 2 - unicode.wlen(title) / 2), titleTextBox.y + y - 1, colors.titleText, title)
	end
end

local saveFileButton = window:addChild(GUI.adaptiveRoundedButton(titleTextBox.localX - 11, 2, 2, 0, colors.panel, colors.panelSelectionText, colors.panelSelectionText, colors.panel, "Save"))
local openFileButton = window:addChild(GUI.adaptiveRoundedButton(saveFileButton.localX - 10, 2, 2, 0, colors.panel, colors.panelSelectionText, colors.panelSelectionText, colors.panel, "Open"))

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

local function load(path)
	local file, reason = filesystem.open(path, "rb")
	
	if file then
		bytes = {}
		
		local byte
		while true do
			byte = file:readBytes(1)
			if byte then
				table.insert(bytes, byte)
			else
				break
			end
		end

		file:close()
		
		offset = 0
		selection.from, selection.to = 1, 1
		scrollBar.value, scrollBar.maximumValue = 0, #bytes
		
		updateTitles()
	else
		GUI.alert("Failed to open file for reading: " .. tostring(reason))
	end
end

openFileButton.onTouch = function()
	local filesystemDialog = GUI.addFilesystemDialog(workspace, true, 50, math.floor(workspace.height * 0.8), "Open", "Cancel", "File name", "/")
	
	filesystemDialog:setMode(GUI.IO_MODE_OPEN, GUI.IO_MODE_FILE)
	filesystemDialog:show()
	
	filesystemDialog.onSubmit = function(path)
		load(path)

		config.recentPath = path
		filesystem.writeTable(configPath, config)

		workspace:draw()
	end
end

saveFileButton.onTouch = function()
	local filesystemDialog = GUI.addFilesystemDialog(workspace, true, 50, math.floor(workspace.height * 0.8), "Save", "Cancel", "File name", "/")
	
	filesystemDialog:setMode(GUI.IO_MODE_SAVE, GUI.IO_MODE_FILE)
	filesystemDialog:show()
	
	filesystemDialog.onSubmit = function(path)
		local file = filesystem.open(path, "wb")
		
		if file then
			for i = 1, #bytes do
				file:write(string.char(bytes[i]))
			end
			
			file:close()
		else
			GUI.alert("Failed to open file for writing: " .. tostring(reason))
		end
	end
end

window.onResize = function(width, height)
	byteField.height = height - 6
	charField.height = byteField.height
	window.backgroundPanel.height = height - 4
	scrollBar.height = window.backgroundPanel.height
	verticalCounter.height = window.backgroundPanel.height + 1
	separator.height = byteField.height + 2
end

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

window.onResize(window.width, window.height)

local args, options = system.parseArguments(...)

load(((options.o or options.open) and args[1] and filesystem.exists(args[1])) and args[1] or config.recentPath)

workspace:draw()
