
-- 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( tCustomCode )
	if edu.hasProgram() then
		local ok, tLinesOrErrors
		if tCustomCode then
		    ok, tLinesOrErrors = true, tCustomCode
		else
		    ok, tLinesOrErrors = edu.getProgram( false )
		end
		if ok then
		    edu.clearSyntaxErrors()
			tLines = tLinesOrErrors
			if #tLines == 0 then
				table.insert( tLines, "" )
			end
			bSyntaxHighlight = true
			bReadOnly = false
		else
		    edu.setSyntaxErrors( tLinesOrErrors )
		    if #tLinesOrErrors > 1 then
    			tLines = { "Errors in program:" }
    		else
    		    tLines = { "Error in program:" }
    		end
			for n,tError in ipairs(tLinesOrErrors) do
			    table.insert( tLines, tError.message )
			end
			bSyntaxHighlight = false
			bReadOnly = true
		end
	else
        edu.clearSyntaxErrors()
		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( tCustomCode )
	load( tCustomCode )
	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, tCustomCode )
	local ok, tLinesOrErrors
	if tCustomCode then
	    ok, tLinesOrErrors = true, tCustomCode
	else
	    ok, tLinesOrErrors = edu.getProgram( true )
	end
	if ok then
		local sProgram = ""
		for n,sLine in ipairs( tLinesOrErrors ) 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, {
			    {
			        message = sErrorMessage,
			        firstSlot = 1,
			        lastSlot = 1,
			        firstLine = nErrorLine,
			        lastLine = nErrorLine,
			    }
			}
		end
			return false, {
			    {
			        message = "Syntax Error",
			        firstSlot = 1,
			        lastSlot = 1,
			    }
			}
	end
	return false, tLinesOrErrors
end

-- Actual program functionality begins
local ok, err = pcall( function()
    load()
end )
if not ok then
    printError( err )
    return
end

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

redrawText()

function run( bDebug, tCustomCode )
    if tCustomCode then
        reload( tCustomCode )
    end

    -- Construct a debugging environment for the program
    local bPaused = bDebug
    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, tErrors
    do
        local ok, p1 = compile( tEnv, tCustomCode )
        if ok then
            fnProgram = p1
        else
            tErrors = p1
        end
    end

    -- Setup the program
    if fnProgram then
        edu.setCurrentSlot( 1 )
        edu.clearRuntimeError()
        edu.setPaused( bPaused )
    else
        edu.setCurrentSlot( tErrors[1].firstSlot )
        edu.setRuntimeError( tErrors[1].message )
        edu.setPaused( true )
        nHighlightLine = tErrors[1].firstLine
    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
                    sleep( 0.4 )
                    success = true

                else
                    -- If it errored, extract the line number
                    local sErrorLine, sErrorMessage
                    if sError then
                        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.setRuntimeError( 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.clearRuntimeError()
    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()
end
	
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
		    -- Run (or debug)
		    run( sEvent == "edu_debug" )

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

		elseif sEvent == "turtle_inventory" then
			-- The turtles inventory changed, so reload the program
			reload()
			
	    elseif sEvent == "edu_forward" then
            run( false, { "turtle.forward()" } )

	    elseif sEvent == "edu_back" then
            run( false, { "turtle.back()" } )

	    elseif sEvent == "edu_turn_left" then
            run( false, { "turtle.turnLeft()" } )

	    elseif sEvent == "edu_turn_right" then
            run( false, { "turtle.turnRight()" } )

	    elseif sEvent == "edu_up" then
            run( false, { "turtle.up()" } )

	    elseif sEvent == "edu_down" then
            run( false, { "turtle.down()" } )

	    end
	end
end

local ok, err = pcall( function()
    -- Handle input
    mainLoop()
end )

-- Cleanup
term.clear()
term.setCursorBlink( false )
term.setCursorPos( 1, 1 )
if not ok then
    printError( err )
    return
end
