
-- 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 sHighlightError = nil
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 errorColour = colours.red

local function loadProgram( tCustomCode )
    local ok, tLinesOrErrors
    if tCustomCode then
        ok, tLinesOrErrors = true, tCustomCode
    elseif edu.hasProgram() then
        ok, tLinesOrErrors = edu.getProgram( nil, "none" )
    else
		tLines = { "" }
		bReadOnly = true
		return
    end

    if ok then
        tLines = tLinesOrErrors
        if #tLines == 0 then
            table.insert( tLines, "" )
        end
        bSyntaxHighlight = true
        bReadOnly = false
    else
        local tErrors = tLinesOrErrors
        if #tErrors > 1 then
            tLines = { "Errors in program:" }
        else
            tLines = { "Error in program:" }
        end
        for n,tError in ipairs( tErrors ) do
            table.insert( tLines, tError.message )
        end
        bSyntaxHighlight = false
        bReadOnly = true
    end
end

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

local function revert()
	edu.clearManualProgram()
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, bg )
	local match = string.match( sLine, regex )
	if match then
		if type(colour) == "number" then
			term.setTextColour( colour )
		else
			term.setTextColour( colour(match) )
		end
		if bg then
		    term.setBackgroundColor( bg )
		end
		term.write( match )
		term.setTextColour( textColour )
        term.setBackgroundColor( bgColour )
		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, "^\?\?\?", textColour, errorColour ) or
			tryWrite( sLine, "^[^%w_]", textColour )
	end
end

local tCompletions
local nCompletion

local tCompleteEnv = {
    [ "commands" ] = commands,
    [ "turtle" ] = turtle,
    [ "turtleedu" ] = turtleedu,
    [ "redstone" ] = redstone,
    [ "rs" ] = rs,
    [ "peripheral" ] = peripheral,
    [ "sleep" ] = sleep,
    [ "exec" ] = exec,

    [ "math" ] = math,
    [ "os" ] = os,
    [ "string" ] = string,
    [ "table" ] = table,

    [ "assert" ] = assert,
    [ "error" ] = error,
    [ "ipairs" ] = pairs,
    [ "next" ] = next,
    [ "pairs" ] = pairs,
    [ "tonumber" ] = tonumber,
    [ "tostring" ] = tostring,
    [ "type" ] = type,
}

local function complete( sLine )
	if settings.get( "edu.autocomplete", true ) then
	    local nStartPos = string.find( sLine, "[a-zA-Z0-9_%.]+$" )
	    if nStartPos then
	        sLine = string.sub( sLine, nStartPos )
	    end
	    if #sLine > 0 then
	        return textutils.complete( sLine, tCompleteEnv )
	    end
	end
    return nil
end

local function recomplete()
    local sLine = tLines[y]
    if not bMenu and not bReadOnly and x == string.len(sLine) + 1 then
        tCompletions = complete( sLine )
        if tCompletions and #tCompletions > 0 then
            nCompletion = 1
        else
            nCompletion = nil
        end
    else
        tCompletions = nil
        nCompletion = nil
    end
end

local function writeCompletion( sLine )
    if nCompletion then
        local sCompletion = tCompletions[ nCompletion ]
        term.setTextColor( colours.white )
        term.setBackgroundColor( colours.grey )
        term.write( sCompletion )
        term.setTextColor( textColour )
        term.setBackgroundColor( bgColour )
    end
end

local function redrawText()
	term.setBackgroundColor( bgColour )
	term.setTextColor( textColour )
	term.clear()

    local cursorX, cursorY = x, y
	for y=1,h - 1 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 sHighlightError then
					term.setBackgroundColour( debugErrorBgColour )
				else
					term.setBackgroundColour( debugBgColour )
				end
				term.clearLine()
				term.write( sLine )
				term.setBackgroundColour( bgColour )
			else
				writeHighlighted( sLine )
                if cursorY == y and cursorX == #sLine + 1 then
                    writeCompletion()
                end
			end
		end
	end
	term.setCursorPos( x - scrollX, y - scrollY )
end

local function redrawLine(_nY)
	local sLine = tLines[_nY]
	if sLine then
        term.setCursorPos( 1 - scrollX, _nY - scrollY )
        term.clearLine()
        writeHighlighted( sLine )
        if _nY == y and x == #sLine + 1 then
            writeCompletion()
        end
        term.setCursorPos( x - scrollX, y - scrollY )
    end
end

local function redrawFooter()
    term.setCursorPos( 1, h )
    term.clearLine()
    if nHighlightLine then
        if sHighlightError then
            term.setTextColour( errorColour )
            term.write( sHighlightError )
            term.setTextColour( textColour )
        end
    end
	term.setCursorPos( x - scrollX, y - scrollY )
end

local function setCursor( newX, newY )
    local oldX, oldY = x, y
    x, y = newX, newY
	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 - 1) then
		scrollY = y - (h - 1)
		screenY = (h - 1)
		bRedraw = true
	end
	
	recomplete()
	if bRedraw then
		redrawText()
	elseif y ~= oldY then
	    redrawLine( oldY )
	    redrawLine( y )
	else
	    redrawLine( y )
	end
	term.setCursorPos( screenX, screenY )
end

function reload( tCustomCode )
	loadProgram( tCustomCode )
	term.setCursorBlink( true )
	
	local bMoved = false
	local newX, newY = x, y
	if y > #tLines then
		newY = #tLines
		bMoved = true
	end
	if x > #tLines[newY] + 1 then
		newX = #tLines[newY] + 1
		bMoved = true
	end
	if bMoved then
		setCursor( newX, newY )
	else
	    recomplete()
	end
	redrawText()
	redrawFooter()
end

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

function compile( tEnvironment, sNameOrtCode, sAnnotationLevel )
	local ok, tLinesOrErrors
	if type( sNameOrtCode ) == "table" then
	    local tCode = sNameOrtCode
	    ok, tLinesOrErrors = true, tCode
	elseif type( sNameOrtCode ) == "string" then
	    local sName = sNameOrtCode
	    ok, tLinesOrErrors = edu.getProgram( sName, sAnnotationLevel )
	else
	    ok, tLinesOrErrors = edu.getProgram( nil, sAnnotationLevel )
	end

	if ok then
		local sProgram = ""
		for n,sLine in ipairs( tLinesOrErrors ) do
			sProgram = sProgram .. sLine .. "\n"
		end

		local fnProgram, sError = load( sProgram, "program", "t", tEnvironment )
		if fnProgram then
			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,
			        slot = 1,
			        line = nErrorLine,
			    }
			}
		else
			return false, {
			    {
			        message = "Syntax Error",
			        slot = 1,
			    }
			}
	    end
	end
	return false, tLinesOrErrors
end

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

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

recomplete()
redrawText()
redrawFooter()

function run( bDebug, tCustomCode, tCustomCodeAnnotated )
    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()
                    redrawFooter()
                end
                bStopped = bStopped or edu.areTurtlesStopped()
                while bPaused or bStopped do
                    local sEvent = os.pullEvent()
                    bStopped = bStopped or edu.areTurtlesStopped()
                    if bStopped then
                        edu.setStopped()
                    elseif bPaused then
                        edu.setPaused()
                        if sEvent == "edu_step" then
                            break
                        elseif sEvent == "edu_resume" then
                            bPaused = false
                            edu.setRunning()
                            break
                        end
                    end
                end
            end,
        },
    }
    setmetatable( tEnv, { __index = _ENV } )

    -- Setup code to lazy-compile all the other programs in the library
    local tLibraryPrograms = edu.getLibraryPrograms()
    for n=1,#tLibraryPrograms do
        local sLibraryProgram = tLibraryPrograms[n]
        local sFunctionName = sLibraryProgram
        tEnv[ sFunctionName ] = function( ... )
            local ok, p1 = compile( tEnv, sLibraryProgram, "subprogram" )
            if ok then
                local fnLibraryProgram = p1
                tEnv[ sFunctionName ] = fnLibraryProgram
                return fnLibraryProgram( ... )
            else
                error( "Error compiling \"" .. sLibraryProgram .. "\"", 2 );
            end
        end
    end

    -- Compile the program
    local fnProgram, tErrors
    do
        local ok, p1 = compile( tEnv, tCustomCodeAnnotated, "full" )
        if ok then
            fnProgram = p1
        else
            tErrors = p1
        end
    end

    -- Setup the program
    if fnProgram then
        if bPaused then
            edu.setPaused()
            edu.setCurrentSlot(nil)
        else
            edu.setRunning()
            edu.setCurrentSlot(nil)
        end
    else
        local tError = tErrors[1]
        local sErrorMessage = tError.message
        local nErrorSlot = tError.slot
        local nErrorLine = tError.line
        edu.setErrored( sErrorMessage )
        edu.setCurrentSlot( nErrorSlot )
        nHighlightLine = nErrorLine
        sHighlightError = sErrorMessage
        reposition()
    end

    edu.clearVariables()
    term.setCursorBlink( false )
    if nHighlight then
        nSuggestion = nil
        redrawLine(y)
    end


    -- 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
                elseif sEvent == "edu_stop" then
                    bStopped = true
                elseif sEvent == "edu_recall" then
                    bStopped = true
                    bReset = 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 )
                            reposition()
                        end
                    end
                    if sErrorMessage == nil then
                        sErrorMessage = "Error"
                    end
                    edu.setErrored( sErrorMessage )
                    sHighlightError = sErrorMessage
                end
            end

            -- Display the error and wait for a human response
            if not success then
                redrawText()
                redrawFooter()
                while not bStopped do
                    os.pullEvent()
                    bStopped = bStopped or edu.areTurtlesStopped()
                    if bStopped then
                        edu.setStopped()
                    end
                end
            end
        end
    )

    -- Finish the program
    edu.setStopped()
    edu.setCurrentSlot( nil )
    edu.clearVariables()
    if bReset then
        edu.restoreTurtleState()
    end

    -- Reload the program, incase it was changed while we were running it
    nHighlightLine = nil
    sHighlightError = nil
    reload()
end
	
local function acceptCompletion()
    if nCompletion then
        -- Find the common prefix of all the other suggestions which start with the same letter as the current one
        local sCompletion = tCompletions[ nCompletion ]
        tLines[y] = tLines[y] .. sCompletion
        setCursor( x + string.len( sCompletion ), y )
    end
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 nCompletion then
			        -- Cycle completions
                    nCompletion = nCompletion - 1
                    if nCompletion < 1 then
                        nCompletion = #tCompletions
                    end
                    redrawLine(y)

				elseif y > 1 then
					-- Move cursor up
					setCursor(
					    math.min( x, string.len( tLines[y - 1] ) + 1 ),
					    y - 1
					)
				end

			elseif param == keys.down then
				-- Down
			    if nCompletion then
			        -- Cycle completions
                    nCompletion = nCompletion + 1
                    if nCompletion > #tCompletions then
                        nCompletion = 1
                    end
                    redrawLine(y)

				elseif y < #tLines then
				    -- Move cursor down
					setCursor(
                        math.min( x, string.len( tLines[y + 1] ) + 1 ),
                        y + 1
                    )
				end
		
			elseif param == keys.tab then
				-- Tab
				if not bReadOnly then
                    if nCompletion and x == string.len(tLines[y]) + 1 then
                        -- Accept autocomplete
                        acceptCompletion()
                    else
                        -- Indent line
                        local sLine = tLines[y]
                        tLines[y] = string.sub(sLine,1,x-1) .. "  " .. string.sub(sLine,x)
                        setCursor( x + 2, y )
                        save()
                    end
				end
				
			elseif param == keys.pageUp then
				-- Page Up
				-- Move up a page
				local newY
				if y - (h - 1) >= 1 then
					newY = y - (h - 1)
				else
				    newY = 1
				end
                setCursor(
				    math.min( x, string.len( tLines[newY] ) + 1 ),
				    newY
				)

			elseif param == keys.pageDown then
				-- Page Down
				-- Move down a page
				local newY
				if y + (h - 1) <= #tLines then
					newY = y + (h - 1)
				else
					newY = #tLines
				end
				local newX = math.min( x, string.len( tLines[newY] ) + 1 )
				setCursor( newX, newY )

			elseif param == keys.home then
				-- Home
				-- Move cursor to the beginning
				if x > 1 then
                    setCursor(1,y)
                end
		
			elseif param == keys["end"] then
				-- End
				-- Move cursor to the end
				local nLimit = string.len( tLines[y] ) + 1
				if x < nLimit then
    				setCursor( nLimit, y )
    		    end
		
			elseif param == keys.left then
				-- Left
				if x > 1 then
					-- Move cursor left
    				setCursor( x - 1, y )
				elseif x==1 and y>1 then
    				setCursor( string.len( tLines[y-1] ) + 1, y - 1 )
				end

			elseif param == keys.right then
				-- Right
			    local nLimit = string.len( tLines[y] ) + 1
				if x < nLimit then
					-- Move cursor right
					setCursor( x + 1, y )
			    elseif nCompletion and x == string.len(tLines[y]) + 1 then
                    -- Accept autocomplete
                    acceptCompletion()
				elseif x==nLimit and y<#tLines then
				    -- Go to next line
				    setCursor( 1, y + 1 )
				end

			elseif param == keys.delete then
				-- Delete
				if not bReadOnly then
                    local nLimit = string.len( tLines[y] ) + 1
                    if x < nLimit then
                        local sLine = tLines[y]
                        tLines[y] = string.sub(sLine,1,x-1) .. string.sub(sLine,x+1)
                        recomplete()
                        redrawLine(y)
                        save()
                    elseif y<#tLines then
                        tLines[y] = tLines[y] .. tLines[y+1]
                        table.remove( tLines, y+1 )
                        recomplete()
                        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)
                        setCursor( x - 1, 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 )
                        setCursor( sPrevLen + 1, y - 1 )
                        redrawText()
                        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) )
                    setCursor( spaces + 1, y + 1 )
                    redrawText()
					save()
				end

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

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

		elseif sEvent == "mouse_click" then
			-- Click
			if param == 1 then
				-- Left click
				-- Navigate
				local cx,cy = param2, param3
				if cy <= (h - 1) then
					local newY = math.min( math.max( scrollY + cy, 1 ), #tLines )
					local newX = math.min( math.max( scrollX + cx, 1 ), string.len( tLines[newY] ) + 1 )
					setCursor( newX, newY )
				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_recall" 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()" }, { "assert( turtle.forward() )" } )

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

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

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

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

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

	    elseif sEvent == "edu_dig" then
            run( false, { "turtle.dig()" }, { "assert( turtle.dig() )" } )

	    elseif sEvent == "edu_place" then
            run( false, { "turtle.place()" }, { "assert( turtle.place() )" } )

	    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
