
TextEditor = {}

TextEditor.CODE_EDITOR_MODE = 1
TextEditor.TERMINAL_MODE = 2
TextEditor.LOG_MODE = 3

function TextEditor:new()
	TextEditor.__index = TextEditor

	local newInstance = {}
    setmetatable(newInstance, self)
    
    return newInstance
end

function TextEditor:init(mode)
	self.screenWidth = 340.0
	self.screenHeight = 320.0
	self.mode = mode
	self.wrapLines = false
	self.terminalCursorY = 0

	if self.mode == TextEditor.CODE_EDITOR_MODE then
		self:_setupCodeEditor()
	elseif self.mode == TextEditor.TERMINAL_MODE then
		self:_setupTerminal()
	elseif self.mode == TextEditor.LOG_MODE then
		self:_setupLog()
	end

	self.syntaxHilights = {
		{word = "%.%.", color = self.keywordsColor}, --string junction
		{word = "%=%=", color = self.keywordsColor}, --string junction
		{word = "%f[%w]%d+%f[%W]", color = self.namesColor}, --numbers

		-- leave these last to override previous matches
		{word = "([\"'])(.-)%1", color = self.stringColor}, --string
		{word = "%-%-.-\n", color = self.commentColor}, --comment
	}

	self.text = ""
	self.textLines = {}
	self.scrollOffsetX = 0
	self.scrollOffsetY = 0
	self.selectedCharStart = 0
	self.selectedCharEnd = 0
	self.draggingSelStartX = 0
	self.draggingSelStartY = 0
	self.draggingSelEndX = 0
	self.draggingSelEndY = 0
	self.selStartX = 0
	self.selStartY = 0
	self.selEndX = 0
	self.selEndY = 0
	self.keySelectAllowed = false
	self.ctrlKeyDown = false
	self.shiftKeyDown = false

	self.terminalPendingText = ""
	
	self.backgroundSprite = allocSprite()
	self.backgroundSpriteData = {}
	self.backgroundSpriteData.position = vec3(0.0, 0.0, -0.1)
	self.backgroundSpriteData.size = vec2(self.screenWidth, self.screenHeight)
	self.backgroundSpriteData.color = Config.uiColors.terminalBackground
	setSprite(self.backgroundSprite, self.backgroundSpriteData)
	
	self.textSprite = allocSprite()
	self.textSpriteData = {}
	self.textSpriteData.position = vec3(0.0, 0.0, 0.0)
	self.textSpriteData.size = vec2(self.screenWidth, self.screenHeight)
	self.textSpriteData.color = vec4(0.0, 0.0, 0.0, 0.0)
	self.textSpriteData.textColor = Config.uiColors.terminalText

	self.cursorSprite = allocSprite()
	self.cursorSpriteData = {}
	self.cursorSpriteData.position = vec3(0.0, 0.0, 0.1)
	self.cursorSpriteData.size = vec2(1.0, 8.0)
	self.cursorSpriteData.color = Config.uiColors.terminalText

	self.numTilesX = round(self.screenWidth / 8.0) - 1
	self.numTilesY = round(self.screenHeight / 8.0)

	self.selectionLineSprites = {}
	for lineIt = 1, self.numTilesY do
		local selLineSpriteData = {}
		selLineSpriteData.position = vec3(0.0, 8 * (lineIt - 1), 0.0)
		selLineSpriteData.size = vec2(0.0, 8.0)
		selLineSpriteData.color = Config.uiColors.terminalSelection

		local selLineSprite = allocSprite()

		self.selectionLineSprites[lineIt] = {spriteData = selLineSpriteData, sprite = selLineSprite}
	end

	self.cursorX = 0
	self.cursorY = 0
	self.cursorPos = 1
	
	self.cursorBlinkTime = 0.0
	self.cursorVisible = false

	self.time = 0.0
	self.selectScrollTime = 0.0

	if self.testText ~= nil then
		self:setText(self.testText)
	else
		self:setText("")
	end

	self:_setCursorPosXY(0, 0)
end

function TextEditor:setDepth(depth)
	self.backgroundSpriteData.position.z = depth
	setSprite(self.backgroundSprite, self.backgroundSpriteData)

	self.textSpriteData.position.z = depth + 0.1
	setSprite(self.textSprite, self.textSpriteData)

	self.cursorSpriteData.position.z = depth + 0.2
	setSprite(self.cursorSprite, self.cursorSpriteData)

	for lineIt = 1, self.numTilesY do
		self.selectionLineSprites[lineIt].spriteData.position.z = depth + 0.05
		setSprite(self.selectionLineSprites[lineIt].sprite, self.selectionLineSprites[lineIt].spriteData)
	end
end

function TextEditor:_setupCodeEditor()
	Config.uiColors.terminalBackground = Config.paletteNames.greyDark
	Config.uiColors.terminalText = Config.paletteNames.white
	Config.uiColors.terminalComment = Config.paletteNames.grey
	Config.uiColors.terminalString = Config.paletteNames.greyLight
	Config.uiColors.terminalName = Config.paletteNames.greenLight
	Config.uiColors.terminalKeyword = Config.paletteNames.greenLight
	Config.uiColors.terminalSelection = Config.paletteNames.orange

	self.textColor = Config.uiColors.terminalText
	self.commentColor = Config.uiColors.terminalComment
	self.stringColor = Config.uiColors.terminalString
	self.namesColor = Config.uiColors.terminalName
	self.keywordsColor = Config.uiColors.terminalKeyword

	self.namesHilight = {	
		"print",
		"true",
		"false",
		"nil"
	}

	self.keywordsHilight = {	
		"and", "break", "do", "else", "elseif",
	    "end", "for", "function", "goto", "if",
	    "in", "local", "not", "or", "repeat",
	    "return", "then", "until", "while",
	}

	self.syntaxHilights = {
		{word = "%.%.", color = self.keywordsColor}, --string junction
		{word = "%=%=", color = self.keywordsColor}, --string junction
		{word = "%f[%w]%d+%f[%W]", color = self.namesColor}, --numbers

		-- leave these last to override previous matches
		{word = "([\"'])(.-)%1", color = self.stringColor}, --string
		{word = "%-%-.-\n", color = self.commentColor}, --comment
	}
end

function TextEditor:_setupTerminal()
	Config.uiColors.terminalBackground = Config.paletteNames.black
	Config.uiColors.terminalText = Config.paletteNames.white
	Config.uiColors.terminalComment = Config.paletteNames.white
	Config.uiColors.terminalString = Config.paletteNames.white
	Config.uiColors.terminalName = Config.paletteNames.greenLight
	Config.uiColors.terminalKeyword = Config.paletteNames.blue
	Config.uiColors.terminalSelection = Config.paletteNames.orange

	self.textColor = Config.uiColors.terminalText
	self.commentColor = Config.uiColors.terminalComment
	self.stringColor = Config.uiColors.terminalString
	self.namesColor = Config.uiColors.terminalName
	self.keywordsColor = Config.uiColors.terminalKeyword

	self.namesHilight = {}
	self.keywordsHilight = {}
	self.terminalCommands = {}
end

function TextEditor:setNameHighlights(namesToHighlight)
	self.namesHilight = namesToHighlight
end

function TextEditor:setTerminalCommand(commandName, callback)
	self.terminalCommands[commandName] = callback

	self.keywordsHilight[#self.keywordsHilight + 1] = commandName
end

function TextEditor:_setupLog()
	Config.uiColors.terminalBackground = Config.paletteNames.black
	Config.uiColors.terminalText = Config.paletteNames.grey
	Config.uiColors.terminalComment = Config.paletteNames.grey
	Config.uiColors.terminalString = Config.paletteNames.grey
	Config.uiColors.terminalName = Config.paletteNames.greenLight
	Config.uiColors.terminalKeyword = Config.paletteNames.red
	Config.uiColors.terminalSelection = Config.paletteNames.orange

	self.textColor = Config.uiColors.terminalText
	self.commentColor = Config.uiColors.terminalComment
	self.stringColor = Config.uiColors.terminalString
	self.namesColor = Config.uiColors.terminalName
	self.keywordsColor = Config.uiColors.terminalKeyword

	self.namesHilight = {	
		"pw",
	}

	self.keywordsHilight = {	
		"error"
	}
end

function TextEditor:_findPatternMatches(text, pattern)
	local i = 1

	local occurrences = {}

	while true do
	    local start_pos, end_pos, quote, content = string.find(text, pattern, i)
	    if not start_pos then break end

	    occurrences[#occurrences + 1] = {wordStart = start_pos, wordEnd = end_pos}
	    
	    i = end_pos + 1 -- move past the current match
	end

	return occurrences
end

function TextEditor:_findWordMatches(text, word)
	local i = 1

	local occurrences = {}

	while true do
	    local start_pos, end_pos, quote, content = string.find(text, "%f[%w]" .. word .. "%f[%W]", i)
	    if not start_pos then break end

	    occurrences[#occurrences + 1] = {wordStart = start_pos, wordEnd = end_pos}
	    
	    i = end_pos + 1 -- move past the current match
	end

	return occurrences
end

function TextEditor:_colorizeSyntax(text, matchList, textCharColors)
	for i = 1, #matchList do
		local syntaxHilight = self.syntaxHilights[i]

		local word = syntaxHilight.word
		local color = syntaxHilight.color

		local matches = self:_findPatternMatches(text, word)

		for j = 1, #matches do
			local wordPos = matches[j]

			for k = wordPos.wordStart, wordPos.wordEnd do
				textCharColors[k] = color
			end
		end
	end
end

function TextEditor:_colorizeWords(text, matchList, color, textCharColors)
	for i = 1, #matchList do
		local word = matchList[i]

		local matches = self:_findWordMatches(text, word)

		for j = 1, #matches do
			local wordPos = matches[j]

			for k = wordPos.wordStart, wordPos.wordEnd do
				textCharColors[k] = color
			end
		end
	end
end

function TextEditor:_colorizeText(syntaxColorText, textCharColors, textCoordMap)
	self:_colorizeWords(syntaxColorText, self.namesHilight, self.namesColor, textCharColors)
	self:_colorizeWords(syntaxColorText, self.keywordsHilight, self.keywordsColor, textCharColors)
	self:_colorizeSyntax(syntaxColorText, self.syntaxHilights, textCharColors)

	-- transfer colors to the cropped view of our text
	self.textSpriteData.textCharColors = {}

	for y = 1, self.numTilesY do
		local line = self.textLines[y + self.scrollOffsetY]
		
		if line ~= nil then
			local lineMap = textCoordMap[y + self.scrollOffsetY]

			if lineMap == nil then
				break
			end

			for x = 1, self.numTilesX do
				if x > #line then
					break
				else
					local charIt = lineMap[x + self.scrollOffsetX]

					self.textSpriteData.textCharColors[#self.textSpriteData.textCharColors + 1] = textCharColors[charIt]
				end
			end

			self.textSpriteData.textCharColors[#self.textSpriteData.textCharColors + 1] = self.textColor -- return char
		else
			break
		end
	end
end

function TextEditor:_wrapLine(line)
	local trimmedLine = string.sub(line, 1, self.numTilesX) .. "\n"

	local nextLine = string.sub(line, self.numTilesX + 1, #line)
	if #nextLine > self.numTilesX then
		nextLine = self:_wrapLine(nextLine)
	end

	local newLines = trimmedLine .. nextLine .. "\n"

	return newLines
end

function TextEditor:_wrapText(text)
	local wrappedText = ""

	local lines = splitStr(text, "\n")
	for lineIt = 1, #lines do
		local line = lines[lineIt]
		if #line > self.numTilesX then
			wrappedText = wrappedText .. self:_wrapLine(line)
		else
			wrappedText = wrappedText .. line .. "\n"
		end
	end

	return wrappedText
end

function TextEditor:pasteToTerminal(text)
	local forceToWritingLine = true
	self:_forceCursorForTerminal(forceToWritingLine)

	self:_inputText(text)
end

function TextEditor:sendTerminalText(text)
	local forceToWritingLine = true
	self:_forceCursorForTerminal(forceToWritingLine)

	if #self.terminalPendingText == 0 and #self.text > 0 then
		self:_inputText("\n")
	end

	local wrappedText = self:_wrapText(text)

	self.terminalPendingText = wrappedText .. "\n"
end

function TextEditor:addText(text)
	self.text = self.text .. text
	self:setText(text)
end

function TextEditor:setText(text)
	if self.wrapLines == true then
		text = self:_wrapText(text)
	end

	local visibleText = ""
	local syntaxColorText = ""

	self.textLines = splitStr(text, "\n")
	
	if #self.textLines == 0 then
		self.textLines[1] = text
	end

	local textCharColors = {}
	local textCoordMap = {}
	local charIt = 1

	for y = 1, self.numTilesY do
		local line = self.textLines[y + self.scrollOffsetY]
		
		if line ~= nil then
			if #line > 0 then
				self.terminalCursorY = y
			end

			visibleText = visibleText .. string.sub(line, 1 + self.scrollOffsetX, self.numTilesX + self.scrollOffsetX) .. "\n"

			syntaxColorText = syntaxColorText .. line .. "\n"

			local lineMap = {}
			textCoordMap[y + self.scrollOffsetY] = lineMap

			for x = 1, #line do
				lineMap[x] = charIt
				textCharColors[charIt] = self.textColor
				charIt = charIt + 1
			end

			charIt = charIt + 1 -- return char
		else
			break
		end
	end
	
	self.text = text
	self.textSpriteData.text = visibleText

	self:_colorizeText(syntaxColorText, textCharColors, textCoordMap)

	setSprite(self.textSprite, self.textSpriteData)

	-- self:saveDebugSnapshot()
end

function TextEditor:refresh()
	self:setText(self.text)
end

function TextEditor:saveDebugSnapshot()
	local snapshot = "=== TextEditor Debug Snapshot ===\n"
	snapshot = snapshot .. "time: " .. tostring(self.time) .. "\n"
	snapshot = snapshot .. "mode: " .. tostring(self.mode) .. "\n"
	snapshot = snapshot .. "screenWidth: " .. tostring(self.screenWidth) .. "\n"
	snapshot = snapshot .. "screenHeight: " .. tostring(self.screenHeight) .. "\n"
	snapshot = snapshot .. "numTilesX: " .. tostring(self.numTilesX) .. "\n"
	snapshot = snapshot .. "numTilesY: " .. tostring(self.numTilesY) .. "\n"
	snapshot = snapshot .. "scrollOffsetX: " .. tostring(self.scrollOffsetX) .. "\n"
	snapshot = snapshot .. "scrollOffsetY: " .. tostring(self.scrollOffsetY) .. "\n"
	snapshot = snapshot .. "cursorX: " .. tostring(self.cursorX) .. "\n"
	snapshot = snapshot .. "cursorY: " .. tostring(self.cursorY) .. "\n"
	snapshot = snapshot .. "cursorPos: " .. tostring(self.cursorPos) .. "\n"
	snapshot = snapshot .. "wrapLines: " .. tostring(self.wrapLines) .. "\n"
	snapshot = snapshot .. "terminalCursorY: " .. tostring(self.terminalCursorY) .. "\n"
	snapshot = snapshot .. "selectedCharStart: " .. tostring(self.selectedCharStart) .. "\n"
	snapshot = snapshot .. "selectedCharEnd: " .. tostring(self.selectedCharEnd) .. "\n"

	snapshot = snapshot .. "\n=== Text (raw) ===\n"
	snapshot = snapshot .. tostring(self.text) .. "\n"

	snapshot = snapshot .. "\n=== Text Lines ===\n"
	snapshot = snapshot .. "total lines: " .. tostring(#self.textLines) .. "\n"
	for i = 1, #self.textLines do
		local line = self.textLines[i]
		snapshot = snapshot .. "line[" .. tostring(i) .. "] len=" .. tostring(#line) .. ": |" .. line .. "|\n"
	end

	snapshot = snapshot .. "\n=== Visible Text (sprite) ===\n"
	snapshot = snapshot .. tostring(self.textSpriteData.text) .. "\n"

	snapshot = snapshot .. "\n=== textCharColors ===\n"
	if self.textSpriteData.textCharColors then
		snapshot = snapshot .. "count: " .. tostring(#self.textSpriteData.textCharColors) .. "\n"
	else
		snapshot = snapshot .. "nil\n"
	end

	saveFile("textEditorDebug.txt", -1, snapshot)
end

function TextEditor:_cursorTileToStrPos(x, y)
	y = round(clamp(y + 1.0, 1.0, #self.textLines))

	local cursorPos = 0
	for i = 1, y + self.scrollOffsetY - 1 do
		local line = self.textLines[i]
		if line ~= nil then
			cursorPos = cursorPos + #line + 1
		else
			break
		end
	end

	local lineNumChars = 0
	
	local line = self.textLines[y + self.scrollOffsetY]
	if line ~= nil then
		lineNumChars = #line
	end

	x = round(clamp(x + 1, 1.0, lineNumChars + 1 - self.scrollOffsetX))
	cursorPos = cursorPos + x

	return {x - 1, y - 1, cursorPos - 1 + self.scrollOffsetX}
end

function TextEditor:_setCursorPos(charPos, x, y)
	if y < 0 then
		self.scrollOffsetY = self.scrollOffsetY + y
		self:refresh()
	end

	self.cursorPos = round(clamp(charPos, 1, #self.text))
	self.cursorX = round(clamp(x, 0.0, self.numTilesX - 1))
	self.cursorY = round(clamp(y, 0.0, self.numTilesY - 1))
	
	self.cursorSpriteData.position.x = self.cursorX * 8
	self.cursorSpriteData.position.y = self.cursorY * 8

	setSprite(self.cursorSprite, self.cursorSpriteData)

	self.cursorVisible = true
	self.cursorBlinkTime = self.time
end

function TextEditor:_setCursorPosXY(x, y)
	local numTilesX = round(self.screenWidth / 8)
	local numTilesY = round(self.screenHeight / 8)

	local tileX = round(clamp(x, 0, numTilesX - 1))
	local tileY = round(clamp(y, 0, numTilesY - 1))

	local cursor = self:_cursorTileToStrPos(tileX, tileY)
	local cursorX = cursor[1]
	local cursorY = cursor[2]
	local cursorPos = cursor[3]

	self:_setCursorPos(cursorPos, cursorX, cursorY)
	
	if cursorX < 0 then
		local line = self.textLines[cursorY + 1 + self.scrollOffsetY]
		self.scrollOffsetX = #line
		cursorX = 0

		self:_setCursorPos(cursorPos, cursorX, cursorY)

		self:refresh()
	end

	if self.shiftKeyDown == true and self.keySelectAllowed == true then
		self.draggingSelEndX = self.cursorX
		self.draggingSelEndY = self.cursorY
		
		self.draggingSelection = true
		self:_refreshSelectedText()
		self:_scrollSelect()
	end
end

function TextEditor:_scrollPage(offset)
	if offset < 0 then
		if self.scrollOffsetY <= 0 then
			return
		end
	end

	self.scrollOffsetY = self.scrollOffsetY + round(clamp(offset * 100.0, -1.0, 1.0))
	self.scrollOffsetY = round(clamp(self.scrollOffsetY, 0.0, 999.0))
	
	self:_clearSelectedText()
	
	self:refresh()
	if self.mode == TextEditor.TERMINAL_MODE then
		self:_forceCursorForTerminal(true)
	else
		self:_setCursorPosXY(self.cursorX, self.cursorY - offset)
	end
end

function TextEditor:_clearSelectedText()
	self.draggingSelStartX = 0
	self.draggingSelStartY = 0
	
	self.draggingSelEndX = 0
	self.draggingSelEndY = 0

	self.selStartX = 0
	self.selStartY = 0

	self.selEndX = 0
	self.selEndY = 0

	self.selectedCharStart = 0
	self.selectedCharEnd = 0

	for y = 1, self.numTilesY do
		local selLineSprite = self.selectionLineSprites[y]
		selLineSprite.spriteData.position.x = 0.0
		selLineSprite.spriteData.size.x = 0.0

		setSprite(selLineSprite.sprite, selLineSprite.spriteData)
	end
end

function TextEditor:_refreshSelectedText()
	local selStartY = round(min(self.draggingSelStartY, self.draggingSelEndY))
	local selEndY = round(max(self.draggingSelStartY, self.draggingSelEndY))

	local selStartX = self.draggingSelStartX + self.scrollOffsetX
	local selEndX = self.draggingSelEndX + self.scrollOffsetX

	if self.draggingSelEndY < self.draggingSelStartY then
		selStartX = self.draggingSelEndX + self.scrollOffsetX
		selEndX = self.draggingSelStartX + self.scrollOffsetX
	end

	if selStartY == selEndY then
		local tmpStart = selStartX
		local tmpEnd = selEndX

		selStartX = round(min(tmpStart, tmpEnd))
		selEndX = round(max(tmpStart, tmpEnd))
	end

	local lineStart = self.textLines[selStartY + 1 + self.scrollOffsetY]
	local lineBoundsStart = selStartX
	if lineStart then
		lineBoundsStart = #lineStart
	end

	selStartX = round(min(selStartX, lineBoundsStart))

	local lineEnd = self.textLines[selEndY + 1 + self.scrollOffsetY]
	local lineBoundsEnd = 0
	if lineEnd then
		lineBoundsEnd = #lineEnd
	end
	selEndX = round(min(selEndX, lineBoundsEnd))

	for y = 1, self.numTilesY do
		local selLineSprite = self.selectionLineSprites[y]
		selLineSprite.spriteData.position.x = 0.0
		selLineSprite.spriteData.size.x = 0.0
	end

	if selEndY > selStartY then
		-- if selection spans across multiple lines

		self.selectedCharStart = 0
		self.selectedCharEnd = 0
		
		for y = 1, self.scrollOffsetY + selStartY do
			if self.textLines[y] ~= nil then
				self.selectedCharStart = self.selectedCharStart + #self.textLines[y] + 1
			end
		end
		
		self.selectedCharStart = self.selectedCharStart + selStartX + 1
		self.selectedCharEnd = self.selectedCharStart + 1
		
		for y = selStartY, selEndY - 1 do
			local selLineSpriteStartY = round(clamp(y, 0.0, self.numTilesY))

			local selLineSprite = self.selectionLineSprites[selLineSpriteStartY + 1]
			
			local line = self.textLines[y + 1 + self.scrollOffsetY]
			local lineBounds = 0
			if line then
				lineBounds = #line
			end
			selLineSprite.spriteData.size.x = 8.0 * (lineBounds - self.scrollOffsetX)

			self.selectedCharEnd = self.selectedCharEnd + lineBounds + 1
		end

		if selStartY >= 0 then
			local selLineSprite = self.selectionLineSprites[selStartY + 1]
			if selLineSprite ~= nil then
				selLineSprite.spriteData.position.x = 8.0 * (selStartX - self.scrollOffsetX)
				if self.textLines[selStartY + 1 + self.scrollOffsetY] ~= nil then
					selLineSprite.spriteData.size.x = (8 * (#self.textLines[selStartY + 1 + self.scrollOffsetY] - self.scrollOffsetX)) - selLineSprite.spriteData.position.x			
				end
			end
		end

		local selLineSprite = self.selectionLineSprites[selEndY + 1]
		if selLineSprite ~= nil then
			selLineSprite.spriteData.size.x = 8.0 * (selEndX - self.scrollOffsetX)

			self.selectedCharEnd = self.selectedCharEnd + selEndX - selStartX - 1
		end

		if self.selectedCharEnd - self.selectedCharStart > 0 then
			if self.draggingSelEndY < self.draggingSelStartY then
				self:_setCursorPos(self.selectedCharEnd, selStartX - self.scrollOffsetX, selStartY)
			else
				self:_setCursorPos(self.selectedCharEnd, selEndX - self.scrollOffsetX, selEndY)
			end
		end

		selStartX  = selStartX - self.scrollOffsetX

	elseif selEndY == selStartY and abs(selStartX  - selEndX) > 0.0 then
		-- if selection is on single line

		self.selectedCharStart = 0
		self.selectedCharEnd = 0

		for y = 1, self.scrollOffsetY + selEndY do
			local line = self.textLines[y]
			local lineBounds = 0
			if line then
				lineBounds = #line
			end

			self.selectedCharStart = self.selectedCharStart + lineBounds + 1
		end
		self.selectedCharStart = self.selectedCharStart + selStartX + 1
		self.selectedCharEnd = self.selectedCharStart

		selStartX = round(min(self.draggingSelStartX, self.draggingSelEndX))
		selEndX = round(max(self.draggingSelStartX, self.draggingSelEndX))

		selStartXScrolled = round(min(self.draggingSelStartX, self.draggingSelEndX)) + self.scrollOffsetX
		selEndXScrolled = round(max(self.draggingSelStartX, self.draggingSelEndX)) + self.scrollOffsetX

		local lineStart = self.textLines[selStartY + 1 + self.scrollOffsetY]
		local lineBoundsStart = selStartX
		if lineStart then
			lineBoundsStart = #lineStart
		end
		
		if selEndXScrolled > lineBoundsStart then
			selEndXScrolled = lineBoundsStart
		end

		local selLineSprite = self.selectionLineSprites[selStartY + 1]
		selLineSprite.spriteData.position.x = 8.0 * selStartX
		selLineSprite.spriteData.size.x = (8.0 * selEndXScrolled) - (8.0 * selStartXScrolled)
		
		self.selectedCharEnd = self.selectedCharStart + (selEndXScrolled - selStartXScrolled)

		if self.selectedCharEnd - self.selectedCharStart > 0 then
			if self.draggingSelEndX < self.draggingSelStartX then
				self:_setCursorPos(self.selectedCharEnd, selStartXScrolled - self.scrollOffsetX, selEndY)
			else
				self:_setCursorPos(self.selectedCharEnd, selEndXScrolled - self.scrollOffsetX, selEndY)
			end
		end
	end

	for y = 1, self.numTilesY do
		local selLineSprite = self.selectionLineSprites[y]
		setSprite(selLineSprite.sprite, selLineSprite.spriteData)
	end

	if self.draggingSelection == false then
		self.selStartX = selStartX
		self.selStartY = selStartY

		self.selEndX = selEndX
		self.selEndY = selEndY
	end
end

function TextEditor:_scrollSelect()
	-- select scroll right
	if self.draggingSelEndX > self.numTilesX - 2 then
		local deltaTime = self.time - self.selectScrollTime

		if self.shiftKeyDown == true then
			deltaTime = 1.0
		end

		if deltaTime > 0.1 then
			local line = self.textLines[self.cursorY + self.scrollOffsetY + 1]

			if line ~= nil and self.cursorX + self.scrollOffsetX < #line then
				self.scrollOffsetX = self.scrollOffsetX + 1
				self.draggingSelStartX = self.draggingSelStartX - 1

				if self.shiftKeyDown == true then
					self.draggingSelStartX = self.draggingSelStartX - 1
				end

				self:refresh()
			end

			self.selectScrollTime = self.time
		end

		return
	end
	
	-- select scroll left
	if self.draggingSelEndX < 2 and self.scrollOffsetX > 0.0 then
		local deltaTime = self.time - self.selectScrollTime

		if self.shiftKeyDown == true then
			deltaTime = 1.0
		end

		if deltaTime > 0.1 then
			local line = self.textLines[self.cursorY + self.scrollOffsetY + 1]

			if line ~= nil then
				self.scrollOffsetX = self.scrollOffsetX - 1
				self.draggingSelStartX = self.draggingSelStartX + 1

				if self.shiftKeyDown == true then
					self.draggingSelStartX = self.draggingSelStartX + 1
				end

				self:refresh()
			end

			self.selectScrollTime = self.time
		end

		return
	end

	-- select scroll down
	if self.draggingSelEndY > self.numTilesY - 2 then
		local deltaTime = self.time - self.selectScrollTime

		if self.shiftKeyDown == true then
			deltaTime = 1.0
		end

		if deltaTime > 0.1 then
			self.scrollOffsetY = self.scrollOffsetY + 1
			self.draggingSelStartY = self.draggingSelStartY - 1

			if self.shiftKeyDown == true then
				self.draggingSelStartY = self.draggingSelStartY - 1
			end

			self:refresh()

			self.selectScrollTime = self.time
		end

		return
	end

	-- select scroll up
	if self.draggingSelStartY > self.draggingSelEndY and self.draggingSelEndY < 1 and self.scrollOffsetY > 0.0 then
		local deltaTime = self.time - self.selectScrollTime

		if self.shiftKeyDown == true then
			deltaTime = 1.0
		end

		if deltaTime > 0.1 then
			self.scrollOffsetY = self.scrollOffsetY - 1
			self.draggingSelStartY = self.draggingSelStartY + 1

			if self.shiftKeyDown == true then
				self.draggingSelStartY = self.draggingSelStartY + 1
			end

			self:refresh()

			self.selectScrollTime = self.time
		end

		return
	end
end

function TextEditor:_updateMouse()
	if #self.terminalPendingText > 0 then
		return
	end

	self.mousePos = getMousePosition()
	self.mousePressed = getMouseButtonPressed(MOUSE_BUTTON_LEFT)
	self.mouseDown = getMouseButtonDown(MOUSE_BUTTON_LEFT)

	self.mouseInsideWindow = false
	
	if self.mousePos.x > 0.0 and self.mousePos.x < self.screenWidth and 
		self.mousePos.y > 0.0 and self.mousePos.y < self.screenHeight then

		self.mouseInsideWindow = true
	end

	if self.mousePressed == true and self.mouseInsideWindow == true then
		self:_clearSelectedText()

		local cursorX = round(clamp((self.mousePos.x + 2.0) / 8.0, 0.0, self.screenWidth / 8))
		local cursorY = round(clamp((self.mousePos.y - 4.0) / 8.0, 0.0, self.screenHeight / 8))

		if self.mode == TextEditor.TERMINAL_MODE then
			if cursorY > self.terminalCursorY - 1 then
				cursorY = self.terminalCursorY - 1
			end 
		end

		self:_setCursorPosXY(cursorX, cursorY)
		
		self.draggingSelStartX = cursorX
		self.draggingSelStartY = cursorY

		self.draggingSelEndX = cursorX
		self.draggingSelEndY = cursorY

		self.draggingSelection = true

		self:_refreshSelectedText()
	end

	if self.mouseDown == true and self.draggingSelection == true then
		self.draggingSelEndX = round(clamp((self.mousePos.x + 2.0) / 8.0, 0.0, self.screenWidth / 8))
		self.draggingSelEndY = round(clamp((self.mousePos.y - 4.0) / 8.0, 0.0, self.screenHeight / 8))

		if self.mode == TextEditor.TERMINAL_MODE then
			if self.draggingSelEndY > self.terminalCursorY - 1 then
				self.draggingSelEndY = self.terminalCursorY - 1
			end 
		end

		self:_refreshSelectedText()

		if self:_hasSelection() == true then
			self:_scrollSelect()
		end
	end

	if self.draggingSelection == true and self.mouseDown == false then
		self.draggingSelection = false

		self:_refreshSelectedText()
	end

	local scrollWheel = getMouseWheelScrolled()
	if scrollWheel ~= 0.0 then
		self:_scrollPage(scrollWheel)
	end

	local trackpadPan = getTrackpadPanned()
	if trackpadPan.y ~= 0.0 then
		self:_scrollPage(trackpadPan.y * -1)
	end

	local pinchZoom = getTrackpadPinchZoom()
	if pinchZoom ~= 0.0 then
		self:_scrollPage(-pinchZoom)
	end
end

function TextEditor:_findWordDelimiter(horizontalOffset)
	local str = self.textLines[self.cursorY + 1 + self.scrollOffsetY]
	local i = self.cursorX + 1 + self.scrollOffsetX

	if horizontalOffset < 0.0 then
		i = i - 1
	end

    local step = horizontalOffset
    local count = 0

    local breakOnAlphanumeric = true
    local c = str:sub(i, i)
    if not c:match("[%w]") then
    	breakOnAlphanumeric = false
    end
    
    while i >= 1 and i <= #str do
        local c = str:sub(i, i)
		
		if breakOnAlphanumeric == true then
	        if not c:match("[%w]") then
	            break
	        end
	    else
	    	if c:match("[%w]") then
	            break
	        end
	    end

        count = count + 1
        i = i + step
    end

    local newHorizontalOffset = 0
    if horizontalOffset > 0.0 then
    	newHorizontalOffset = count
    else
    	newHorizontalOffset = - count
    end

    return newHorizontalOffset
end

function TextEditor:_moveCursor(horizontalOffset, verticalOffset)
	if self.ctrlKeyDown == true then
		if horizontalOffset ~= 0 and verticalOffset == 0 then
			horizontalOffset = self:_findWordDelimiter(horizontalOffset)
		end
	end

	local cursorX = self.cursorX + horizontalOffset
	local cursorY = self.cursorY + verticalOffset
	
	-- if moving cursor left and text is scrolled to the right, scroll text one char to the left
	if cursorX < 0.0 and self.scrollOffsetX > 0.0 then
		self.scrollOffsetX = self.scrollOffsetX - abs(horizontalOffset)

		self:_setCursorPosXY(cursorX, cursorY)
		
		self:refresh()

		return
	end

	-- if moving cursor right and text is scrolled to the right, scroll one char to the right if within current line bounds
	if cursorX > self.numTilesX - 1 and self.textLines[cursorY + 1 + self.scrollOffsetY] ~= nil then
		if cursorX + self.scrollOffsetX <= #self.textLines[cursorY + 1 + self.scrollOffsetY] then
			self.scrollOffsetX = self.scrollOffsetX + abs(horizontalOffset)

			self:_setCursorPosXY(cursorX, cursorY)
			
			self:refresh()

			return
		end
	end

	-- if moving cursor up, scroll one char up
	if cursorY < 0.0 and self.scrollOffsetY > 0.0 then
		self.scrollOffsetY = self.scrollOffsetY - 1

		self:_setCursorPosXY(cursorX, cursorY)
		
		self:refresh()

		return
	end

	-- if moving cursor down, scroll one char down
	if cursorY > self.numTilesY - 1 then
		self.scrollOffsetY = self.scrollOffsetY + 1

		self:_setCursorPosXY(cursorX, cursorY)
		
		self:refresh()

		return
	end

	-- if going all the way to the left, then wrap around and move up one line
	if horizontalOffset < 0.0 and cursorX < 0.0 then
		cursorY = cursorY - 1
		cursorX = self.numTilesX
	end

	-- if going all the way to the right, then wrap around and move down one line
	if horizontalOffset > 0.0 and self.textLines[cursorY + 1 + self.scrollOffsetY] ~= nil then
		if cursorX > #self.textLines[cursorY + 1 + self.scrollOffsetY] then
			cursorY = cursorY + 1
			cursorX = 0
		end
	end

	self:_setCursorPosXY(cursorX, cursorY)
end

function TextEditor:_inputText(textInput)
	if self:_hasSelection() then
		local text = self.text:sub(1, self.selectedCharStart - 1) .. textInput ..  self.text:sub(self.selectedCharEnd, #self.text - 1)
		
		self:setText(text)

		if textInput == "\n" then
			self:_setCursorPosXY(0, self.cursorY + 1)
		else
			self:_setCursorPos(self.selectedCharStart, self.selStartX + 1, self.selStartY)
		end
		
		self:_clearSelectedText()
	else
		-- special case offset if we are returning while the cursor is at the very start of the buffer
		local offset = 0
		if self.cursorPos == 1 and self.cursorY == 0 and textInput == "\n" then
			offset = 1
		end

		local text = self.text:sub(1, self.cursorPos - offset) .. textInput .. self.text:sub(self.cursorPos + 1 - offset)

		self:setText(text)

		if textInput:sub(1, 1) == "\n" then
			local shouldRefresh = false
			if self.cursorY == self.numTilesY - 1 then
				self.scrollOffsetY = self.scrollOffsetY + 1
				shouldRefresh = true
			end

			if self.scrollOffsetX > 0 then
				self.scrollOffsetX = 0
				shouldRefresh = true
			end

			if shouldRefresh == true then
				self:refresh()
			end

			self:_setCursorPosXY(#textInput - 1, self.cursorY + 1)
		else
			self:_moveCursor(#textInput, 0)

			if self.scrollOffsetX > 0 then
				self.scrollOffsetX = 0
				shouldRefresh = true
			end

			if shouldRefresh == true then
				self:refresh()
			end
		end
	end
end

function TextEditor:getSelectedText()
	local selectedText = self.text:sub(self.selectedCharStart, self.selectedCharEnd - 1)
	
	return selectedText
end

function TextEditor:_hasSelection()
	return abs(self.selectedCharEnd - self.selectedCharStart) > 0.0
end

function TextEditor:_hasMultiLineSelection()
	if abs(self.selStartY - self.selEndY) > 0.0 then
		return true
	end

	return false
end

function TextEditor:_delCharBack()
	if self.mode == TextEditor.TERMINAL_MODE then
		local commandLine = self.textLines[self.terminalCursorY + self.scrollOffsetY]
		
		if #commandLine == 1 then
			return
		end
	end

	if self:_hasSelection() == true then
		local text = self.text:sub(1, self.selectedCharStart - 1) .. self.text:sub(self.selectedCharEnd, #self.text)
		
		self:setText(text)
		self:_setCursorPos(self.selectedCharStart - 1, self.selStartX, self.selStartY)
		
		self:_clearSelectedText()
	else
		local text = self.text:sub(1, self.cursorPos - 1) .. self.text:sub(self.cursorPos + 1, #self.text)
		
		self:setText(text)
		self:_moveCursor(-1, 0)
	end
end

function TextEditor:_delCharFront()
	if self:_hasSelection() == true then
		local text = self.text:sub(1, self.selectedCharStart - 1) .. self.text:sub(self.selectedCharEnd, #self.text)
		
		self:setText(text)
		self:_setCursorPos(self.selectedCharStart, self.selStartX, self.selStartY)
		
		self:_clearSelectedText()
	else
		local text = self.text:sub(1, self.cursorPos) .. self.text:sub(self.cursorPos + 2, #self.text)

		self:setText(text)
	end
end

function TextEditor:_tabIndentMultiLine()
	local lastLineNumChars = 0
	for y = self.selStartY + self.scrollOffsetY, self.selEndY + self.scrollOffsetY do
		local line = self.textLines[y + 1]
		if line then
			if #line > 0 then
				self.textLines[y + 1] = "	" .. line
			end

			lastLineNumChars = #line
		else
			break
		end
	end

	local text = ""
	for y = 1, #self.textLines do
		text = text .. self.textLines[y] .. "\n"
	end

	self:setText(text)

	if self.draggingSelEndY > self.draggingSelStartY then
		self.draggingSelStartX = 0
		self.draggingSelEndX = lastLineNumChars
	else
		self.draggingSelStartX = lastLineNumChars
		self.draggingSelEndX = 0
	end

	self:_refreshSelectedText()
end

function TextEditor:_tabDeindentMultiLine()
	local lastLineNumChars = 0
	for y = self.selStartY + self.scrollOffsetY, self.selEndY + self.scrollOffsetY do
		local line = self.textLines[y + 1]
		if line then
			if #line > 0 then
				local firstChar = line:sub(1, 1)
				if firstChar == "	" or firstChar == " " then -- tab or space
					self.textLines[y + 1] = line:sub(2, #line)
				end
			end

			lastLineNumChars = #line
		else
			break
		end
	end

	local text = ""
	for y = 1, #self.textLines do
		text = text .. self.textLines[y] .. "\n"
	end

	self:setText(text)

	self.draggingSelStartY = self.selStartY
	self.draggingSelEndY = self.selEndY

	if self.draggingSelEndY > self.draggingSelStartY then
		self.draggingSelStartX = 0
		self.draggingSelEndX = lastLineNumChars
	else
		self.draggingSelStartX = lastLineNumChars
		self.draggingSelEndX = 0
	end

	self:_refreshSelectedText()
end

function TextEditor:_deintendLine()
	local line = self.textLines[self.cursorY + 1 + self.scrollOffsetY]
	local firstChar = line:sub(1, 1)
	
	if firstChar == "	" or firstChar == " " then -- tab or space
		self.textLines[self.cursorY + 1 + self.scrollOffsetY] = line:sub(2)

		local text = ""
		for y = 1, #self.textLines do
			text = text .. self.textLines[y] .. "\n"
		end

		self:setText(text)
		self:_moveCursor(-1, 0)
	end
end

function TextEditor:_tabIndent()
	if self:_hasMultiLineSelection() then
		if self.shiftKeyDown == true then
			self:_tabDeindentMultiLine()
		else
			self:_tabIndentMultiLine()
		end
	else
		if self.shiftKeyDown == true then
			self:_deintendLine()
		else
			self:_inputText("	")
		end
	end
end

function TextEditor:_newLine()
	local line = self.textLines[self.cursorY + 1 + self.scrollOffsetY]
	
	local indent = ""
	for i = 1, #line do
		local char = line:sub(i, i)
		if char == "	" or char == " " then -- tab or space
			indent = indent .. "	"
		else
			break
		end
	end

	self:_inputText("\n" .. indent)
end

function TextEditor:_updateKeyboardCodeEditor()
	self.lastKeyPressed = getLastKeyPressed()

	local shiftDown = getKeyDown(KEY_SHIFT)

	if self.shiftKeyDown == false and shiftDown == true then
		self.draggingSelStartX = self.cursorX
		self.draggingSelStartY = self.cursorY
	end
	
	self.shiftKeyDown = shiftDown
	self.ctrlKeyDown = getKeyDown(KEY_CTRL)
	self.altKeyDown = getKeyDown(KEY_ALT)

	self.keySelectAllowed = false

	if self.lastKeyPressed == -1 then
		return
	end

	local textInput = getTextInput()
	if textInput ~= "" then
		self:_inputText(textInput)

	else
		if self.lastKeyPressed == KEY_LEFT then
			self.keySelectAllowed = true
			if self.shiftKeyDown == false then
				self:_clearSelectedText()
			end

			self:_moveCursor(-1, 0)

		elseif self.lastKeyPressed == KEY_RIGHT then
			self.keySelectAllowed = true
			if self.shiftKeyDown == false then
				self:_clearSelectedText()
			end

			self:_moveCursor(1, 0)

		elseif self.lastKeyPressed == KEY_UP then
			self.keySelectAllowed = true
			if self.shiftKeyDown == false then
				self:_clearSelectedText()
			end

			self:_moveCursor(0, -1)

		elseif self.lastKeyPressed == KEY_DOWN then
			self.keySelectAllowed = true
			if self.shiftKeyDown == false then
				self:_clearSelectedText()
			end

			self:_moveCursor(0, 1)

		elseif self.lastKeyPressed == KEY_ENTER or self.lastKeyPressed == KEY_RETURN then
			self:_newLine()

		elseif self.lastKeyPressed == KEY_TAB then
			self:_tabIndent()

		elseif self.lastKeyPressed == KEY_BACKSPACE then
			self:_delCharBack()

		elseif self.lastKeyPressed == KEY_DELETE then
			self:_delCharFront()
		end
	end
end

function TextEditor:_forceCursorForTerminal(forceToWritingLine)
	local cursorX = self.cursorX
	local cursorY = self.cursorY

	if cursorY > self.terminalCursorY - 1 then
		cursorY = self.terminalCursorY - 1
	end 

	if forceToWritingLine == true then
		if cursorX < 1 then
			cursorX = 1
		end

		cursorY = self.terminalCursorY - 1
	end
	
	self:_setCursorPosXY(cursorX, cursorY)
end

function TextEditor:_updateKeyboardTerminal()
	if #self.terminalPendingText > 0 then
		return
	end

	self.lastKeyPressed = getLastKeyPressed()

	local textInput = getTextInput()

	if textInput ~= "" then
		local forceToWritingLine = true
		self:_forceCursorForTerminal(forceToWritingLine)

		self:_inputText(textInput)

	else
		if self.lastKeyPressed == KEY_ENTER or self.lastKeyPressed == KEY_RETURN then
			self:_execTerminalCommand()

		elseif self.lastKeyPressed == KEY_BACKSPACE then
			self:_delCharBack()

		end
	end
end

function TextEditor:_execTerminalCommand()
	local input = self.textLines[self.cursorY + 1 + self.scrollOffsetY]:sub(2)

	local spaceIdx = input:find(" ")
	local command, arg
	if spaceIdx then
		command = input:sub(1, spaceIdx - 1)
		arg = input:sub(spaceIdx + 1)
	else
		command = input
		arg = nil
	end

	local callback = self.terminalCommands[command]
	if callback ~= nil then
		local resStr = callback(arg)
		if resStr ~= nil then
			self:sendTerminalText(resStr)
		end
	else
		self:sendTerminalText("\nunknown command '" .. command .. "'")
	end
end

function TextEditor:_updateKeyboard()
	if self.mode == TextEditor.CODE_EDITOR_MODE then
		self:_updateKeyboardCodeEditor()
	elseif self.mode == TextEditor.TERMINAL_MODE then
		self:_updateKeyboardTerminal()
	end
end

function TextEditor:_updateCursor()
	local deltaTime = self.time - self.cursorBlinkTime
	if deltaTime >= 0.5 then
		self.cursorVisible = not self.cursorVisible
		self.cursorBlinkTime = self.time
	end

	if #self.terminalPendingText > 0 then
		self.cursorVisible = false		
	end

	if self.cursorVisible == true then
		drawSprite2D(self.cursorSprite)
	end
end

function TextEditor:_updateTerminal()
	if #self.terminalPendingText == 0 then
		return
	end

	for i = 1, 4 do
		if #self.terminalPendingText > 0 then
			local newChar = self.terminalPendingText:sub(1, 1)

			self.terminalPendingText = self.terminalPendingText:sub(2)

			self:_inputText(newChar)

			if #self.terminalPendingText == 0 and self.mode == TextEditor.TERMINAL_MODE then
				self:_inputText(">")
			end
		else
			break
		end
	end
end

function TextEditor:update()
	self.time = getTime()
	
	self:_updateMouse()
	self:_updateKeyboard()

	drawSprite2D(self.backgroundSprite)

	for i = 1, self.numTilesY do
		local selLineSprite = self.selectionLineSprites[i]

		if  selLineSprite.spriteData.size.x > 0.0 then
			setSprite(selLineSprite.sprite, selLineSprite.spriteData)
			drawSprite2D(selLineSprite.sprite)
		end
	end

	drawSprite2D(self.textSprite)

	if self.mode == TextEditor.CODE_EDITOR_MODE or self.mode == TextEditor.TERMINAL_MODE then
		self:_updateCursor()
	end

	if self.mode == TextEditor.TERMINAL_MODE or self.mode == TextEditor.LOG_MODE then
		self:_updateTerminal()
	end
end
