
-- Terminal stuff
local x,y = 1,1
local w,h = term.getSize()
local scrollX, scrollY = 0,0

local tLines = {}
local bRunning = true
local nHighlightLine = nil
local bHighlightAsError = false
local bSyntaxHighlight = true

-- Colours
local bgColour = colours.black
local textColour = colours.white
local keywordColour = colours.green
local commentColour = colours.yellow
local stringColour = colours.red
local debugColour = colours.white
local debugBgColour = colours.grey
local debugErrorBgColour = colours.red

local function load()
	if edu.hasProgram() then
		local ok, tLinesOrsError, nFirstErrorSlot, nLastErrorSlot = edu.getProgram( false )
		if ok then
		    edu.clearError()
			tLines = tLinesOrsError
			if #tLines == 0 then
				table.insert( tLines, "" )
			end
			bSyntaxHighlight = true
			bReadOnly = false
		else
		    edu.setError( tLinesOrsError, nFirstErrorSlot, nLastErrorSlot )
			tLines = { "Error in program:", tLinesOrsError }
			bSyntaxHighlight = false
			bReadOnly = true
		end
	else
        edu.clearError()
		tLines = { "" }
		bReadOnly = true
	end
end

local function save()
	edu.setManualProgram( unpack(tLines) )
end

local function revert()
	edu.revertManualProgram()
end

local tKeywords = {
	["and"] = true,
	["break"] = true,
	["do"] = true,
	["else"] = true,
	["elseif"] = true,
	["end"] = true,
	["false"] = true,
	["for"] = true,
	["function"] = true,
	["if"] = true,
	["in"] = true,
	["local"] = true,
	["nil"] = true,
	["not"] = true,
	["or"] = true,
	["repeat"] = true,
	["return"] = true,
	["then"] = true,
	["true"] = true,
	["until"]= true,
	["while"] = true,
}

local function tryWrite( sLine, regex, colour )
	local match = string.match( sLine, regex )
	if match then
		if type(colour) == "number" then
			term.setTextColour( colour )
		else
			term.setTextColour( colour(match) )
		end
		term.write( match )
		term.setTextColour( textColour )
		return string.sub( sLine, string.len(match) + 1 )
	end
	return nil
end

local function writeHighlighted( sLine )
	if not bSyntaxHighlight then
		term.setTextColor( textColour )
		term.write( sLine )
		return
	end	
	
	while string.len(sLine) > 0 do	
		sLine = 
			tryWrite( sLine, "^%-%-%[%[.-%]%]", commentColour ) or
			tryWrite( sLine, "^%-%-.*", commentColour ) or
			tryWrite( sLine, "^\".-[^\\]\"", stringColour ) or
			tryWrite( sLine, "^\'.-[^\\]\'", stringColour ) or
			tryWrite( sLine, "^%[%[.-%]%]", stringColour ) or
			tryWrite( sLine, "^[%w_]+", function( match )
				if tKeywords[ match ] then
					return keywordColour
				end
				return textColour
			end ) or
			tryWrite( sLine, "^[^%w_]", textColour )
	end
end

local function redrawText()
	term.setBackgroundColor( bgColour )
	term.clear()
	
	term.setTextColor( textColour )
	for y=1,h do
		term.setCursorPos( 1 - scrollX, y )
		local nLine = y + scrollY
		local sLine = tLines[ nLine ]
		if sLine ~= nil then
			if nLine == nHighlightLine then
				term.setTextColour( debugColour )
				if bHighlightAsError then
					term.setBackgroundColour( debugErrorBgColour )
				else
					term.setBackgroundColour( debugBgColour )
				end
				term.clearLine()
				writeHighlighted( sLine )
				term.setBackgroundColour( bgColour )
			else
				writeHighlighted( sLine )
			end
		end
	end
	term.setCursorPos( x - scrollX, y - scrollY )
end

local function redrawLine(_nY)
	local sLine = tLines[_nY]
	term.setCursorPos( 1 - scrollX, _nY - scrollY )
	term.clearLine()
	writeHighlighted( sLine )
	term.setCursorPos( x - scrollX, _nY - scrollY )
end

local function setCursor( x, y )
	local screenX = x - scrollX
	local screenY = y - scrollY
	
	local bRedraw = false
	if screenX < 1 then
		scrollX = x - 1
		screenX = 1
		bRedraw = true
	elseif screenX > w then
		scrollX = x - w
		screenX = w
		bRedraw = true
	end
	
	if screenY < 1 then
		scrollY = y - 1
		screenY = 1
		bRedraw = true
	elseif screenY > h then
		scrollY = y - h
		screenY = h
		bRedraw = true
	end
	
	if bRedraw then
		redrawText()
	end
	term.setCursorPos( screenX, screenY )
end

function reload()
	load()
	term.setCursorBlink( not bReadOnly )
	
	local bMoved = false
	if y > #tLines then
		y = #tLines
		bMoved = true
	end
	if x > #tLines[y] + 1 then
		x = #tLines[y] + 1
		bMoved = true
	end
	if bMoved then
		setCursor( x, y )
	end
	
	redrawText()
end

function reposition()
	if nHighlightLine then
		setCursor( x, nHighlightLine )
	end
end

function compile( tEnvironment )
	local ok, tProgramOrError, nFirstErrorSlot, nLastErrorSlot = edu.getProgram( true )
	if ok then
		local sProgram = ""
		for n,sLine in ipairs( tProgramOrError ) do
			sProgram = sProgram .. sLine .. "\n"
		end
	
		local fnProgram, sError = loadstring( sProgram, "program" )
		if fnProgram then
			setmetatable( tEnvironment, { __index = _G } )
			setfenv( fnProgram, tEnvironment )
			return true, fnProgram
		end
		
		local sErrorLine, sErrorMessage = string.match( sError, "%[string \"program\"%]:(%d+): (.+)" )
		if sErrorLine then
			local nErrorLine = tonumber(sErrorLine)
			nErrorLine = math.min( nErrorLine, #tLines )
			return false, sErrorMessage, 1, 1, nErrorLine
		end
		return false, "Syntax Error", 1, 1, nil
	end
	return false, tProgramOrError, nFirstErrorSlot, nLastErrorSlot, nil
end

-- Actual program functionality begins
load()

term.setBackgroundColour( bgColour )
term.clear()
term.setCursorPos(x,y)
term.setCursorBlink( not bReadOnly )

redrawText()
	
function mainLoop()
	-- Main loop
	while bRunning do
		local sEvent, param, param2, param3 = os.pullEvent()
		if sEvent == "key" then
			if param == keys.up then
				-- Up
				if y > 1 then
					-- Move cursor up
					y = y - 1
					x = math.min( x, string.len( tLines[y] ) + 1 )
					setCursor( x, y )
				end
		
			elseif param == keys.down then
				-- Down
				if y < #tLines then
					-- Move cursor down
					y = y + 1
					x = math.min( x, string.len( tLines[y] ) + 1 )
					setCursor( x, y )
				end
		
			elseif param == keys.tab then
				-- Tab
				if not bReadOnly then
					-- Indent line
					local sLine = tLines[y]
					tLines[y]="  "..sLine
					x = x + 2
					setCursor( x, y )
					redrawLine(y)
					save()
				end
				
			elseif param == keys.pageUp then
				-- Page Up
				-- Move up a page
				local sx,sy=term.getSize()
				y=y-sy-1
				if y<1 then	y=1 end
				x = math.min( x, string.len( tLines[y] ) + 1 )
				setCursor( x, y )
		
			elseif param == keys.pageDown then
				-- Page Down
				-- Move down a page
				local sx,sy=term.getSize()
				if y<#tLines-sy-1 then
					y = y+sy-1
				else
					y = #tLines
				end
				x = math.min( x, string.len( tLines[y] ) + 1 )
				setCursor( x, y )
		
			elseif param == keys.home then
				-- Home
				-- Move cursor to the beginning
				x=1
				setCursor(x,y)
		
			elseif param == keys["end"] then
				-- End
				-- Move cursor to the end
				x = string.len( tLines[y] ) + 1
				setCursor(x,y)
		
			elseif param == keys.left then
				-- Left
				if x > 1 then
					-- Move cursor left
					x = x - 1
				elseif x==1 and y>1 then
					-- Move back a line
					x = string.len( tLines[y-1] ) + 1
					y = y - 1
				end
				setCursor( x, y )

			elseif param == keys.right then
				-- Right
				if x < string.len( tLines[y] ) + 1 then
					-- Move cursor right
					x = x + 1
				elseif x==string.len( tLines[y] ) + 1 and y<#tLines then
					-- Move cursor forward a line
					x = 1
					y = y + 1
				end
				setCursor( x, y )
					
			elseif param == keys.delete then
				-- Delete
				if not bReadOnly then
					if  x < string.len( tLines[y] ) + 1 then
						local sLine = tLines[y]
						tLines[y] = string.sub(sLine,1,x-1) .. string.sub(sLine,x+1)
						redrawLine(y)
						save()
					elseif y<#tLines then
						tLines[y] = tLines[y] .. tLines[y+1]
						table.remove( tLines, y+1 )
						redrawText()
						save()
					end
				end

			elseif param == keys.backspace then
				-- Backspace
				if not bReadOnly then
					if x > 1 then
						-- Remove character
						local sLine = tLines[y]
						tLines[y] = string.sub(sLine,1,x-2) .. string.sub(sLine,x)
						redrawLine(y)
		
						x = x - 1
						setCursor( x, y )
						save()
					
					elseif y > 1 then
						-- Remove newline
						local sPrevLen = string.len( tLines[y-1] )
						tLines[y-1] = tLines[y-1] .. tLines[y]
						table.remove( tLines, y )
						redrawText()
			
						x = sPrevLen + 1
						y = y - 1
						setCursor( x, y )
						save()
					end
				end
				
			elseif param == keys.enter then
				-- Enter
				if not bReadOnly then
					-- Newline
					local sLine = tLines[y]
					local _,spaces=string.find(sLine,"^[ ]+")
					if not spaces then
						spaces=0
					end
					tLines[y] = string.sub(sLine,1,x-1)
					table.insert( tLines, y+1, string.rep(' ',spaces)..string.sub(sLine,x) )
					redrawText()
		
					x = spaces+1
					y = y + 1
					setCursor( x, y )
					save()
				end

			end
			
		elseif sEvent == "char" then
			-- Input text
			if not bReadOnly then
				local sLine = tLines[y]
				tLines[y] = string.sub(sLine,1,x-1) .. param .. string.sub(sLine,x)
				redrawLine(y)
	
				x = x + string.len( param )
				setCursor( x, y )
				save()
			end

		elseif sEvent == "mouse_click" then
			-- Click
			if param == 1 then
				-- Left click
				-- Navigate
				local cx,cy = param2, param3
				if cy < h then
					y = math.min( math.max( scrollY + cy, 1 ), #tLines )
					x = math.min( math.max( scrollX + cx, 1 ), string.len( tLines[y] ) + 1 )
					setCursor( x, y )
				end
			end
	
		elseif sEvent == "mouse_scroll" then
			-- Scroll wheel
			if param == -1 then
				-- Scroll up
				if scrollY > 0 then
					-- Move cursor up
					scrollY = scrollY - 1
					redrawText()
				end
			elseif param == 1 then
				-- Scroll down
				local nMaxScroll = #tLines - (h-1)
				if scrollY < nMaxScroll then
					-- Move cursor down
					scrollY = scrollY + 1
					redrawText()
				end
			end
		
		elseif sEvent == "edu_revert" then
			-- Revert the code to its unedited state
			revert()
			reload()
			
		elseif (sEvent == "edu_run" or sEvent == "edu_debug") and edu.hasProgram() then
			-- Construct a debugging environment for the program
			local bPaused = (sEvent == "edu_debug")
			local bStopped = false
			local tEnv = {
				["debug"] = {
					step = function( nLine )
						if nLine ~= nHighlightLine then
							nHighlightLine = nLine
							reposition()
							redrawText()
						end
						while bPaused or bStopped do
							local sEvent = os.pullEvent()
							if bPaused and sEvent == "edu_step" then
								break
							end
						end
					end,
				},
			}
			
			-- Compile the program
			local fnProgram, sErrorMessage, nFirstErrorSlot, nLastErrorSlot, nErrorLine
			do
				local ok, p1, p2, p3, p4 = compile( tEnv )
				if ok then
					fnProgram = p1
				else
					sErrorMessage, nFirstErrorSlot, nLastErrorSlot, nErrorLine = p1, p2, p3, p4
				end
			end

			-- Setup the program
			if fnProgram then
	    		edu.setCurrentSlot( 1 )
	    		edu.clearError()
                edu.setPaused( bPaused )
		    else
	    		edu.setCurrentSlot( nFirstErrorSlot )
                edu.setError( sErrorMessage, nFirstErrorSlot, nLastErrorSlot )
                edu.setPaused( false )
                nHighlightLine = nErrorLine
    		end
    		edu.clearVariables()
			term.setCursorBlink( false )

			-- Save the state
            edu.saveTurtleState()

			-- Run the program
			local bReset = false
			parallel.waitForAny(
				function()
					while not bStopped do
						local sEvent = os.pullEvent()
						if sEvent == "edu_pause" then
							bPaused = true
							edu.setPaused( bPaused )
						elseif sEvent == "edu_resume" then
							bPaused = false
							edu.setPaused( bPaused )
						elseif sEvent == "edu_stop" then
							bStopped = true
					    elseif sEvent == "edu_restore" then
                	        bReset = true
					        bStopped = true
					    end
					end
				end,
				function()
					local success = false
					if fnProgram then
						-- If the program compiled, run it
						local ok, sError = pcall( fnProgram )
						if ok then
							-- If it succeeded, nothing left to do
							success = true
						else
							-- If it errored, extract the line number
							if sError then
								local sErrorLine
								sErrorLine, sErrorMessage = string.match( sError, "program:(%d+): (.+)" )
								if sErrorLine then
									nHighlightLine = tonumber( sErrorLine )
									nHighlightLine = math.min( nHighlightLine, #tLines )
								end
							end
							if sErrorMessage == nil then
								sErrorMessage = "Error"
							end
							edu.setError( sErrorMessage )
                            edu.setPaused( false )
						end
					end
					
					-- Display the error and wait for a human response
					if not success then
						bHighlightAsError = true
						redrawText()
						while not bStopped do
							coroutine.yield()
						end
					end
				end
			)
			
			-- Finish the program
			edu.clearError()
			edu.setCurrentSlot( nil )
			edu.setPaused( false )
    		edu.clearVariables()
    		if bReset then
                edu.restoreTurtleState()
            end

			-- Reload the program, incase it was changed while we were running it
			bHighlightAsError = false
			nHighlightLine = nil
			reload()

	    elseif sEvent == "edu_restore" then
	        -- The user wants to restore the turtle to a saved state
	        edu.restoreTurtleState()

		elseif sEvent == "edu_update" or sEvent == "turtle_inventory" then
			-- The turtles inventory changed, so reload the program
			reload()
			
		end
	end
end

-- Handle input
mainLoop()

-- Cleanup
term.clear()
term.setCursorBlink( false )
term.setCursorPos( 1, 1 )