local parentShell = _ENV.shell

_ENV.shell = { }

local fs         = _G.fs
local shell      = _ENV.shell

local sandboxEnv = setmetatable({ }, { __index = _G })
for k,v in pairs(_ENV) do
	sandboxEnv[k] = v
end
sandboxEnv.shell = shell

_G.requireInjector(_ENV)

local Util = require('util')

local DIR = (parentShell and parentShell.dir()) or ""
local PATH = (parentShell and parentShell.path()) or ".:/rom/programs"
local tAliases = (parentShell and parentShell.aliases()) or {}
local tCompletionInfo = (parentShell and parentShell.getCompletionInfo()) or {}

local bExit = false
local tProgramStack = {}

local function tokenise( ... )
	local sLine = table.concat( { ... }, " " )
	local tWords = {}
	local bQuoted = false
	for match in string.gmatch( sLine .. "\"", "(.-)\"" ) do
		if bQuoted then
			table.insert( tWords, match )
		else
			for m in string.gmatch( match, "[^ \t]+" ) do
				table.insert( tWords, m )
			end
		end
		bQuoted = not bQuoted
	end

	return tWords
end

local function run(env, ...)
	local args = tokenise(...)
	local command = table.remove(args, 1) or error('No such program')
	local isUrl = not not command:match("^(https?:)")

	local path, loadFn
	if isUrl then
		path = command
		loadFn = Util.loadUrl
	else
		path = shell.resolveProgram(command) or error('No such program')
		loadFn = loadfile
	end

	local fn, err = loadFn(path, env)
	if not fn then
		error(err)
	end

	if _ENV.multishell then
		_ENV.multishell.setTitle(_ENV.multishell.getCurrent(), fs.getName(path):match('([^%.]+)'))
	end

	if isUrl then
		tProgramStack[#tProgramStack + 1] = path:match("^https?://([^/:]+:?[0-9]*/?.*)$")
	else
		tProgramStack[#tProgramStack + 1] = path
	end

	local r = { fn(table.unpack(args)) }

	tProgramStack[#tProgramStack] = nil

	return table.unpack(r)
end

-- Install shell API
function shell.run(...)
	local oldTitle

	if _ENV.multishell then
		oldTitle = _ENV.multishell.getTitle(_ENV.multishell.getCurrent())
	end

	local env = setmetatable(Util.shallowCopy(sandboxEnv), { __index = _G })
	local r = { pcall(run, env, ...) }

	if _ENV.multishell then
		_ENV.multishell.setTitle(_ENV.multishell.getCurrent(), oldTitle or 'shell')
	end

	return table.unpack(r)
end

function shell.exit()
	bExit = true
end

function shell.dir() return DIR end
function shell.setDir(d) DIR = d end
function shell.path() return PATH end
function shell.setPath(p) PATH = p end

function shell.resolve( _sPath )
	local sStartChar = string.sub( _sPath, 1, 1 )
	if sStartChar == "/" or sStartChar == "\\" then
		return fs.combine( "", _sPath )
	else
		return fs.combine(DIR, _sPath )
	end
end

function shell.resolveProgram( _sCommand )
	if tAliases[_sCommand] ~= nil then
		_sCommand = tAliases[_sCommand]
	end

	if _sCommand:match("^(https?:)") then
		return _sCommand
	end

	local path = shell.resolve(_sCommand)
	if fs.exists(path) and not fs.isDir(path) then
		return path
	end
	if fs.exists(path .. '.lua') then
		return path .. '.lua'
	end

	-- If the path is a global path, use it directly
	local sStartChar = string.sub( _sCommand, 1, 1 )
	if sStartChar == "/" or sStartChar == "\\" then
		local sPath = fs.combine( "", _sCommand )
		if fs.exists( sPath ) and not fs.isDir( sPath ) then
			return sPath
		end
		return nil
	end

	-- Otherwise, look on the path variable
	for sPath in string.gmatch(PATH or '', "[^:]+") do
		sPath = fs.combine(sPath, _sCommand )
		if fs.exists( sPath ) and not fs.isDir( sPath ) then
			return sPath
		end
		if fs.exists(sPath .. '.lua') then
			return sPath .. '.lua'
		end
	end
	-- Not found
	return nil
end

function shell.programs( _bIncludeHidden )
	local tItems = {}

	-- Add programs from the path
	for sPath in string.gmatch(PATH, "[^:]+") do
		sPath = shell.resolve(sPath)
		if fs.isDir( sPath ) then
			local tList = fs.list( sPath )
			for _,sFile in pairs( tList ) do
				if not fs.isDir( fs.combine( sPath, sFile ) ) and
					(_bIncludeHidden or string.sub( sFile, 1, 1 ) ~= ".") then
					tItems[ sFile ] = true
				end
			end
		end
	end

	-- Sort and return
	local tItemList = {}
	for sItem in pairs( tItems ) do
		table.insert( tItemList, sItem )
	end
	table.sort( tItemList )
	return tItemList
end

local function completeProgram( sLine )
	if #sLine > 0 and string.sub( sLine, 1, 1 ) == "/" then
		-- Add programs from the root
		return fs.complete( sLine, "", true, false )
	else
		local tResults = {}
		local tSeen = {}

		-- Add aliases
		for sAlias in pairs( tAliases ) do
			if #sAlias > #sLine and string.sub( sAlias, 1, #sLine ) == sLine then
				local sResult = string.sub( sAlias, #sLine + 1 )
				if not tSeen[ sResult ] then
					table.insert( tResults, sResult )
					tSeen[ sResult ] = true
				end
			end
		end

		-- Add programs from the path
		local tPrograms = shell.programs()
		for n=1,#tPrograms do
			local sProgram = tPrograms[n]
			if #sProgram > #sLine and string.sub( sProgram, 1, #sLine ) == sLine then
				local sResult = string.sub( sProgram, #sLine + 1 )
				if not tSeen[ sResult ] then
					table.insert( tResults, sResult )
					tSeen[ sResult ] = true
				end
			end
		end

		-- Sort and return
		table.sort( tResults )
		return tResults
	end
end

local function completeProgramArgument( sProgram, nArgument, sPart, tPreviousParts )
	local tInfo = tCompletionInfo[ sProgram ]
	if tInfo then
		return tInfo.fnComplete( shell, nArgument, sPart, tPreviousParts )
	end
	return nil
end

function shell.complete(sLine)
	if #sLine > 0 then
		local tWords = tokenise( sLine )
		local nIndex = #tWords
		if string.sub( sLine, #sLine, #sLine ) == " " then
			nIndex = nIndex + 1
		end
		if nIndex == 1 then
			local sBit = tWords[1] or ""
			local sPath = shell.resolveProgram( sBit )
			if tCompletionInfo[ sPath ] then
				return { " " }
			else
				local tResults = completeProgram( sBit )
				for n=1,#tResults do
					local sResult = tResults[n]
					local cPath = shell.resolveProgram( sBit .. sResult )
					if tCompletionInfo[ cPath ] then
						tResults[n] = sResult .. " "
					end
				end
				return tResults
			end

		elseif nIndex > 1 then
			local sPath = shell.resolveProgram( tWords[1] )
			local sPart = tWords[nIndex] or ""
			local tPreviousParts = tWords
			tPreviousParts[nIndex] = nil
			return completeProgramArgument( sPath , nIndex - 1, sPart, tPreviousParts )
		end
	end
end

function shell.completeProgram( sProgram )
	return completeProgram( sProgram )
end

function shell.setCompletionFunction(sProgram, fnComplete)
	tCompletionInfo[sProgram] = { fnComplete = fnComplete }
end

function shell.getCompletionInfo()
	return tCompletionInfo
end

function shell.getRunningProgram()
	return tProgramStack[#tProgramStack]
end

function shell.setEnv(name, value)
	_ENV[name] = value
	sandboxEnv[name] = value
end

function shell.getEnv()
	return sandboxEnv
end

function shell.setAlias( _sCommand, _sProgram )
	tAliases[_sCommand] = _sProgram
end

function shell.clearAlias( _sCommand )
	tAliases[_sCommand] = nil
end

function shell.aliases()
	local tCopy = {}
	for sAlias, sCommand in pairs(tAliases) do
		tCopy[sAlias] = sCommand
	end
	return tCopy
end

function shell.newTab(tabInfo, ...)
	local args = tokenise(...)
	local path = table.remove(args, 1)
	path = shell.resolveProgram(path)

	if path then
		tabInfo.path = path
		tabInfo.env = Util.shallowCopy(sandboxEnv)
		tabInfo.args = args
		tabInfo.title = fs.getName(path):match('([^%.]+)')

		if path ~= 'sys/apps/shell' then
			table.insert(tabInfo.args, 1, tabInfo.path)
			tabInfo.path = 'sys/apps/shell'
		end
		return _ENV.multishell.openTab(tabInfo)
	end
	return nil, 'No such program'
end

function shell.openTab( ... )
	-- needs to use multishell.launch .. so we can run with stock multishell
	local tWords = tokenise( ... )
	local sCommand = tWords[1]
	if sCommand then
		local sPath = shell.resolveProgram(sCommand)
		if sPath == "sys/apps/shell" then
			return _ENV.multishell.launch(Util.shallowCopy(sandboxEnv), sPath, table.unpack(tWords, 2))
		else
			return _ENV.multishell.launch(Util.shallowCopy(sandboxEnv), "sys/apps/shell", sCommand, table.unpack(tWords, 2))
		end
	end
end

function shell.openForegroundTab( ... )
	return shell.newTab({ focused = true }, ...)
end

function shell.openHiddenTab( ... )
	return shell.newTab({ hidden = true }, ...)
end

function shell.switchTab(tabId)
	_ENV.multishell.setFocus(tabId)
end

local tArgs = { ... }
if #tArgs > 0 then
	local env = setmetatable(Util.shallowCopy(sandboxEnv), { __index = _G })
	return run(env, ...)
end

local Config   = require('config')
local Entry    = require('entry')
local History  = require('history')
local Input    = require('input')
local Terminal = require('terminal')

local colors    = _G.colors
local os        = _G.os
local term      = _G.term
local textutils = _G.textutils

local terminal = term.current()
--Terminal.scrollable(terminal, 100)
terminal.noAutoScroll = true

local config = {
	standard = {
		textColor  = colors.white,
		commandTextColor = colors.lightGray,
		directoryTextColor = colors.gray,
		directoryBackgroundColor = colors.black,
		promptTextColor = colors.gray,
		promptBackgroundColor = colors.black,
		directoryColor = colors.gray,
	},
	color = {
		textColor = colors.white,
		commandTextColor = colors.yellow,
		directoryTextColor  = colors.orange,
		directoryBackgroundColor = colors.black,
		promptTextColor = colors.blue,
		promptBackgroundColor = colors.black,
		directoryColor = colors.green,
	},
	displayDirectory = true,
}

Config.load('shellprompt', config)

local _colors = config.standard
if term.isColor() then
	_colors = config.color
end

local function autocompleteArgument(program, words)
	local word = ''
	if #words > 1 then
		word = words[#words]
	end

	local tInfo = tCompletionInfo[program]
	return tInfo.fnComplete(shell, #words - 1, word, words)
end

local function autocompleteAnything(line, words)
	local results = shell.complete(line)

	if results and #results == 0 and #words == 1 then
		results = nil
	end
	if not results then
		results = fs.complete(words[#words] or '', shell.dir(), true, false)
	end

	return results
end

local function autocomplete(line)
	local words = { }
	for word in line:gmatch("%S+") do
		table.insert(words, word)
	end
	if line:match(' $') then
		table.insert(words, '')
	end
	if #words == 0 then
		words = { '' }
	end

	local results

	local program = shell.resolveProgram(words[1])
	if tCompletionInfo[program] then
		results = autocompleteArgument(program, words) or { }
	else
		results = autocompleteAnything(line, words) or { }
	end

	Util.filterInplace(results, function(f)
		return not Util.key(results, f .. '/')
	end)
	local w = words[#words] or ''
	for k,arg in pairs(results) do
		results[k] = w .. arg
	end

	if #results == 1 then
		words[#words] = results[1]
		return table.concat(words, ' ')
	elseif #results > 1 then

		local function someComplete()
			-- ugly (complete as much as possible)
			local word = words[#words] or ''
			local i = #word + 1
			while true do
				local ch
				for _,f in ipairs(results) do
					if #f < i then
						words[#words] = string.sub(f, 1, i - 1)
						return table.concat(words, ' ')
					end
					if not ch then
						ch = string.sub(f, i, i)
					elseif string.sub(f, i, i) ~= ch then
						if i == #word + 1 then
							return
						end
						words[#words] = string.sub(f, 1, i - 1)
						return table.concat(words, ' ')
					end
				end
				i = i + 1
			end
		end

		local t = someComplete()
		if t then
			return t
		end

		print()

		local word = words[#words] or ''
		local prefix = word:match("(.*/)") or ''
		if #prefix > 0 then
			for _,f in ipairs(results) do
				if f:match("^" .. prefix) ~= prefix then
					prefix = ''
					break
				end
			end
		end

		local tDirs, tFiles = { }, { }
		for _,f in ipairs(results) do
			if fs.isDir(shell.resolve(f)) then
				f = f:gsub(prefix, '', 1)
				table.insert(tDirs, f)
			else
				f = f:gsub(prefix, '', 1)
				table.insert(tFiles, f)
			end
		end
		table.sort(tDirs)
		table.sort(tFiles)

		if #tDirs > 0 and #tDirs < #tFiles then
			local tw = term.getSize()
			local nMaxLen = tw / 8
			for _,sItem in pairs(results) do
				nMaxLen = math.max(string.len(sItem) + 1, nMaxLen)
			end
			local w = term.getSize()
			local nCols = math.floor(w / nMaxLen)
			if #tDirs < nCols then
				for _ = #tDirs + 1, nCols do
					table.insert(tDirs, '')
				end
			end
		end

		if #tDirs > 0 then
			textutils.tabulate(_colors.directoryColor, tDirs, colors.white, tFiles)
		else
			textutils.tabulate(colors.white, tFiles)
		end

		term.setTextColour(_colors.promptTextColor)
		term.setBackgroundColor(_colors.promptBackgroundColor)
		term.write("$ " )

		term.setTextColour(_colors.commandTextColor)
		term.setBackgroundColor(colors.black)
		return line
	end
end

local function shellRead(history)
	local lastLen = 0
	local entry = Entry({
		width = term.getSize() - 3
	})

	history:reset()
	term.setCursorBlink(true)

	local function redraw()
		local _,cy = term.getCursorPos()
		term.setCursorPos(3, cy)
		local filler = #entry.value < lastLen
			and string.rep(' ', lastLen - #entry.value)
			or ''
		local str = string.sub(entry.value, entry.scroll + 1)
		term.write(string.sub(str, 1, entry.width) .. filler)
		term.setCursorPos(3 + entry.pos - entry.scroll, cy)
		lastLen = #entry.value
	end

	while true do
		local event, p1, p2, p3 = os.pullEventRaw()

		local ie = Input:translate(event, p1, p2, p3)
		if ie then
			if ie.code == 'scroll_up' then
				--terminal.scrollUp()

			elseif ie.code == 'scroll_down' then
				--terminal.scrollDown()

			elseif ie.code == 'terminate' then
				bExit = true
				break

			elseif ie.code == 'enter' then
				break

			elseif ie.code == 'up' or ie.code == 'down' then
				if ie.code == 'up' then
					entry.value = history:back() or ''
				else
					entry.value = history:forward() or ''
				end
				entry.pos = string.len(entry.value)
				entry.scroll = 0
				entry:updateScroll()
				redraw()

			elseif ie.code == 'tab' then
				if entry.pos == #entry.value then
					local cline = autocomplete(entry.value)
					if cline then
						entry.value = cline
						entry.pos = #entry.value
						entry:updateScroll()
						redraw()
					end
				end

			elseif entry:process(ie) then
				redraw()
			end

		elseif event == "term_resize" then
			entry.width = term.getSize() - 3
			redraw()
		end
	end

	--local _, cy = term.getCursorPos()
	--term.setCursorPos( w + 1, cy )
	print()
	term.setCursorBlink( false )
	return entry.value
end

local history = History.load('usr/.shell_history', 25)

while not bExit do
	if config.displayDirectory then
		term.setTextColour(_colors.directoryTextColor)
		term.setBackgroundColor(_colors.directoryBackgroundColor)
		print('==' .. os.getComputerLabel() .. ':/' .. DIR)
	end
	term.setTextColour(_colors.promptTextColor)
	term.setBackgroundColor(_colors.promptBackgroundColor)
	term.write("$ " )
	term.setTextColour(_colors.commandTextColor)
	term.setBackgroundColor(colors.black)
	local sLine = shellRead(history)
	if bExit then -- terminated
		break
	end
	sLine = Util.trim(sLine)
	if #sLine > 0 and sLine ~= 'exit' then
		history:add(sLine)
	end
	term.setTextColour(_colors.textColor)
	if #sLine > 0 then
		local result, err = shell.run(sLine)
		if not result and err then
			_G.printError(err)
		end
	end
end
