-- Get file to edit
local tArgs = { ... }
if #tArgs == 0 then
    print( "Usage: edit <path>" )
    return
end

-- Error checking
local sPath = shell.resolve( tArgs[1] )
local bReadOnly = fs.isReadOnly( sPath )
if fs.exists( sPath ) and fs.isDir( sPath ) then
    print( "Cannot edit a directory." )
    return
end

-- Create .lua files by default
if not fs.exists( sPath ) and not string.find( sPath, "%." ) then
    local sExtension = settings.get("edit.default_extension", "" )
    if sExtension ~= "" and type( sExtension ) == "string" then
        sPath = sPath .. "." .. sExtension
    end
end

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

local tLines = {}
local bRunning = true

-- Colours
local highlightColour, keywordColour, commentColour, textColour, bgColour, stringColour
if term.isColour() then
    bgColour = colours.black
    textColour = colours.white
    highlightColour = colours.yellow
    keywordColour = colours.yellow
    commentColour = colours.green
    stringColour = colours.red
else
    bgColour = colours.black
    textColour = colours.white
    highlightColour = colours.white
    keywordColour = colours.white
    commentColour = colours.white
    stringColour = colours.white
end

-- Menus
local bMenu = false
local nMenuItem = 1
local tMenuItems = {}
if not bReadOnly then
    table.insert( tMenuItems, "Save" )
end
if shell.openTab then
    table.insert( tMenuItems, "Run" )
end
if peripheral.find( "printer" ) then
    table.insert( tMenuItems, "Print" )
end
table.insert( tMenuItems, "Exit" )

local sStatus = "Press Ctrl to access menu"
if string.len( sStatus ) > w - 5 then
    sStatus = "Press Ctrl for menu"
end

local function load( _sPath )
    tLines = {}
    if fs.exists( _sPath ) then
        local file = io.open( _sPath, "r" )
        local sLine = file:read()
        while sLine do
            table.insert( tLines, sLine )
            sLine = file:read()
        end
        file:close()
    end
    
    if #tLines == 0 then
        table.insert( tLines, "" )
    end
end

local function save( _sPath )
    -- Create intervening folder
    local sDir = _sPath:sub(1, _sPath:len() - fs.getName(_sPath):len() )
    if not fs.exists( sDir ) then
        fs.makeDir( sDir )
    end

    -- Save
    local file = nil
    local function innerSave()
        file = fs.open( _sPath, "w" )
        if file then
            for n, sLine in ipairs( tLines ) do
                file.write( sLine .. "\n" )
            end
        else
            error( "Failed to open ".._sPath )
        end
    end
    
    local ok, err = pcall( innerSave )
    if file then 
        file.close()
    end
    return ok, err
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 )
    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, "^\'.-[^\\]\'", 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 tCompletions
local nCompletion

local tCompleteEnv = _ENV
local function complete( sLine )
    if settings.get( "edit.autocomplete" ) 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()
    local cursorX, cursorY = x, y
    for y=1,h-1 do
        term.setCursorPos( 1 - scrollX, y )
        term.clearLine()

        local sLine = tLines[ y + scrollY ]
        if sLine ~= nil then
            writeHighlighted( sLine )
            if cursorY == y and cursorX == #sLine + 1 then
                writeCompletion()
            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, _nY - scrollY )
    end
end

local function redrawMenu()
    -- Clear line
    term.setCursorPos( 1, h )
    term.clearLine()

    -- Draw line numbers
    term.setCursorPos( w - string.len( "Ln "..y ) + 1, h )
    term.setTextColour( highlightColour )
    term.write( "Ln " )
    term.setTextColour( textColour )
    term.write( y )

    term.setCursorPos( 1, h )
    if bMenu then
        -- Draw menu
        term.setTextColour( textColour )
        for nItem,sItem in pairs( tMenuItems ) do
            if nItem == nMenuItem then
                term.setTextColour( highlightColour )
                term.write( "[" )
                term.setTextColour( textColour )
                term.write( sItem )
                term.setTextColour( highlightColour )
                term.write( "]" )
                term.setTextColour( textColour )
            else
                term.write( " "..sItem.." " )
            end
        end
    else
        -- Draw status
        term.setTextColour( highlightColour )
        term.write( sStatus )
        term.setTextColour( textColour )
    end

    -- Reset cursor
    term.setCursorPos( x - scrollX, y - scrollY )
end

local tMenuFuncs = { 
    Save = function()
        if bReadOnly then
            sStatus = "Access denied"
        else
            local ok, err = save( sPath )
            if ok then
                sStatus="Saved to "..sPath
            else
                sStatus="Error saving to "..sPath
            end
        end
        redrawMenu()
    end,
    Print = function()
        local printer = peripheral.find( "printer" )
        if not printer then
            sStatus = "No printer attached"
            return
        end

        local nPage = 0
        local sName = fs.getName( sPath )
        if printer.getInkLevel() < 1 then
            sStatus = "Printer out of ink"
            return
        elseif printer.getPaperLevel() < 1 then
            sStatus = "Printer out of paper"
            return
        end

        local screenTerminal = term.current()
        local printerTerminal = {
            getCursorPos = printer.getCursorPos,
            setCursorPos = printer.setCursorPos,
            getSize = printer.getPageSize,
            write = printer.write,
        }
        printerTerminal.scroll = function()
            if nPage == 1 then
                printer.setPageTitle( sName.." (page "..nPage..")" )            
            end
            
            while not printer.newPage()    do
                if printer.getInkLevel() < 1 then
                    sStatus = "Printer out of ink, please refill"
                elseif printer.getPaperLevel() < 1 then
                    sStatus = "Printer out of paper, please refill"
                else
                    sStatus = "Printer output tray full, please empty"
                end
    
                term.redirect( screenTerminal )
                redrawMenu()
                term.redirect( printerTerminal )
                
                local timer = os.startTimer(0.5)
                sleep(0.5)
            end

            nPage = nPage + 1
            if nPage == 1 then
                printer.setPageTitle( sName )
            else
                printer.setPageTitle( sName.." (page "..nPage..")" )
            end
        end
        
        bMenu = false
        term.redirect( printerTerminal )
        local ok, error = pcall( function()
            term.scroll()
            for n, sLine in ipairs( tLines ) do
                print( sLine )
            end
        end )
        term.redirect( screenTerminal )
        if not ok then
            print( error )
        end
        
        while not printer.endPage() do
            sStatus = "Printer output tray full, please empty"
            redrawMenu()
            sleep( 0.5 )
        end
        bMenu = true
            
        if nPage > 1 then
            sStatus = "Printed "..nPage.." Pages"
        else
            sStatus = "Printed 1 Page"
        end
        redrawMenu()
    end,
    Exit = function()
        bRunning = false
    end,
    Run = function()
        local sTempPath = "/.temp"
        local ok, err = save( sTempPath )
        if ok then
            local nTask = shell.openTab( sTempPath )
            if nTask then
                shell.switchTab( nTask )
            else
                sStatus="Error starting Task"
            end
            fs.delete( sTempPath )
        else
            sStatus="Error saving to "..sTempPath
        end
        redrawMenu()
    end
}

local function doMenuItem( _n )
    tMenuFuncs[tMenuItems[_n]]()
    if bMenu then
        bMenu = false
        term.setCursorBlink( true )
    end
    redrawMenu()
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 )

    redrawMenu()
end

-- Actual program functionality begins
load(sPath)

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

recomplete()
redrawText()
redrawMenu()

local function acceptCompletion()
    if nCompletion then
        -- Append the completion
        local sCompletion = tCompletions[ nCompletion ]
        tLines[y] = tLines[y] .. sCompletion
        setCursor( x + string.len( sCompletion ), y )
    end
end

-- Handle input
while bRunning do
    local sEvent, param, param2, param3 = os.pullEvent()
    if sEvent == "key" then
        local oldX, oldY = x, y
        if param == keys.up then
            -- Up
            if not bMenu then
                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
            end

        elseif param == keys.down then
            -- Down
            if not bMenu then
                -- Move cursor 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
            end

        elseif param == keys.tab then
            -- Tab
            if not bMenu and 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 + 4, y )
                end
            end

        elseif param == keys.pageUp then
            -- Page Up
            if not bMenu then
                -- 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
                )
            end

        elseif param == keys.pageDown then
            -- Page Down
            if not bMenu then
                -- 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 )
            end

        elseif param == keys.home then
            -- Home
            if not bMenu then
                -- Move cursor to the beginning
                if x > 1 then
                    setCursor(1,y)
                end
            end

        elseif param == keys["end"] then
            -- End
            if not bMenu then
                -- Move cursor to the end
                local nLimit = string.len( tLines[y] ) + 1
                if x < nLimit then
                    setCursor( nLimit, y )
                end
            end

        elseif param == keys.left then
            -- Left
            if not bMenu then
                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
            else
                -- Move menu left
                nMenuItem = nMenuItem - 1
                if nMenuItem < 1 then
                    nMenuItem = #tMenuItems
                end
                redrawMenu()
            end

        elseif param == keys.right then
            -- Right
            if not bMenu then
                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
            else
                -- Move menu right
                nMenuItem = nMenuItem + 1
                if nMenuItem > #tMenuItems then
                    nMenuItem = 1
                end
                redrawMenu()
            end

        elseif param == keys.delete then
            -- Delete
            if not bMenu and 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)
                elseif y<#tLines then
                    tLines[y] = tLines[y] .. tLines[y+1]
                    table.remove( tLines, y+1 )
                    recomplete()
                    redrawText()
                end
            end

        elseif param == keys.backspace then
            -- Backspace
            if not bMenu and not bReadOnly then
                if x > 1 then
                    -- Remove character
                    local sLine = tLines[y]
                    if x > 4 and string.sub(sLine,x-4,x-1) == "    " and not string.sub(sLine, 1, x - 1):find("%S") then
                        tLines[y] = string.sub(sLine,1,x-5) .. string.sub(sLine,x)
                        setCursor( x - 4, y )
                    else
                        tLines[y] = string.sub(sLine,1,x-2) .. string.sub(sLine,x)
                        setCursor( x - 1, y )
                    end
                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()
                end
            end

        elseif param == keys.enter then
            -- Enter
            if not bMenu and 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()

            elseif bMenu then
                -- Menu selection
                doMenuItem( nMenuItem )

            end

        elseif param == keys.leftCtrl or param == keys.rightCtrl or param == keys.rightAlt then
            -- Menu toggle
            bMenu = not bMenu
            if bMenu then
                term.setCursorBlink( false )
            else
                term.setCursorBlink( true )
            end
            redrawMenu()

        end
        
    elseif sEvent == "char" 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 + 1, y )

        elseif bMenu then
            -- Select menu items
            for n,sMenuItem in ipairs( tMenuItems ) do
                if string.lower(string.sub(sMenuItem,1,1)) == string.lower(param) then
                    doMenuItem( n )
                    break
                end
            end
        end

    elseif sEvent == "paste" then
        if not bReadOnly then
            -- Close menu if open
            if bMenu then
                bMenu = false
                term.setCursorBlink( true )
                redrawMenu()
            end
            -- 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 )
        end
        
    elseif sEvent == "mouse_click" then
        if not bMenu then
            if param == 1 then
                -- Left click
                local cx,cy = param2, param3
                if cy < h 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
        end
        
    elseif sEvent == "mouse_scroll" then
        if not bMenu then
            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
        end

    elseif sEvent == "term_resize" then
        w,h = term.getSize()
        setCursor( x, y )
        redrawMenu()
        redrawText()

    end
end

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