--[[
	cLinux : Lore of the Day!
	Made by Piorjade, daelvn

	NAME:        /bin/packman
	CATEGORY:    Binary
	SET:         Extra Binary
	VERSION:     01:alpha0
	DESCRIPTION: 
		This program manages repositories and their programs
		
	All credits to lyqyd.
]]--


if lib.perm.permission.check("/bin") == "x" then
	printError("User is not root, aborting...")
	return
end

package = {}


list = {}
installed = {}
installRoot = "/"
local unpack = unpack or table.unpack

local function postStatus(type, text)
	os.queueEvent("package_status", type, text)
	while true do
		local event = {os.pullEvent("package_status")}
		if event[1] == "package_status" then break end
	end
end

local function printInformation(text)
	postStatus("info", text)
end

local function printWarning(text)
	postStatus("warning", text)
end

local function printError(text)
	postStatus("error", text)
end

local function findFileEntry(fileList, path)
	local entryFound = false
	for i = 1, #fileList do
		if fileList[i].path == path then
			entryFound = i
			break
		end
	end
	return entryFound
end

local function updateFileInfo(fileList, path, version)
	local entry = findFileEntry(fileList, path)
	if entry then
		fileList[entry].path = path
		fileList[entry].version = version
	else
		table.insert(fileList, {path = path, version = version})
	end
end

local transactionQueue = {}
local currentPackage

local Transaction = {
	writeFile = function(self)
		local path = fs.combine(installRoot, self.path)
		if fs.exists(path) then
			local handle = io.open(path, "r")
			if handle then
				self.backup = handle:read("*a")
				handle:close()
			end
		end
		local handle = io.open(path, "w")
		if handle then
			printInformation("Writing file "..path)
			handle:write(self.contents)
			handle:close()
		else
			return false
		end
		return true
	end,
	deleteFile = function(self)
		local path = fs.combine(installRoot, self.path)
		if fs.exists(path) then
			local handle = io.open(path, "r")
			if handle then
				self.backup = handle:read("*a")
				handle:close()
			end
		end
		printInformation("Deleting file "..path)
		fs.delete(path)
		return not fs.exists(path)
	end,
	makeDirectory = function(self)
		local path = fs.combine(installRoot, self.path)
		if not fs.exists(path) then
			printInformation("Creating directory "..path)
			fs.makeDir(path)
		end
		return fs.isDir(path)
	end,
	removeDirectory = function(self)
		local path = fs.combine(installRoot, self.path)
		if fs.exists(path) and fs.isDir(path) and #(fs.list(path)) == 0 then
			printInformation("Removing directory "..path)
			fs.delete(path)
		end
		return not fs.exists(path)
	end,
	updateInfo = function(self)
		newLine = self.path..(self.version and ";"..self.version or "")
		local lineFound = false
		for i = 1, #self.contents do
			if self.path == string.match(self.contents[i], "^([^;]+)") then
				--found the right entry, modify correctly now.
				lineFound = true
				self.contents[i] = newLine
				updateFileInfo(installed[self.pack.fullName].files, self.path, self.version)
				break
			end
		end
		if not lineFound and newLine then
			--didn't find a matching line in the loop, add a new line at the end.
			table.insert(self.contents, newLine)
		end
	end,
	removeInfo = function(self)
		for i = 1, #self.contents do
			if self.path == string.match(self.contents[i], "^([^;]+)") then
				table.remove(self.contents, i)
				local entry = findFileEntry(installed[self.pack.fullName].files, self.path)
				if entry then
					table.remove(installed[self.pack.fullName].files, entry)
				end
				break
			end
		end
	end,
}

function Transaction.finish(self)
	if Transaction[self.type] then
		return Transaction[self.type](self)
	else
		return false
	end
end

function Transaction.rollback(self)
	if Transaction[self.type] then
		if self.type == "writeFile" then
			if self.backup ~= nil then
				self.contents = self.backup
				return Transaction.writeFile(self)
			else
				return Transaction.deleteFile(self)
			end
		elseif self.type == "deleteFile" then
			if self.backup ~= nil then
				self.contents = self.backup
				return Transaction.writeFile(self)
			end
		elseif self.type == "makeDirectory" then
			return Transaction.removeDirectory(self)
		elseif self.type == "removeDirectory" then
			return Transaction.makeDirectory(self)
		end
	else
		return false
	end
end

local tmeta = {__index = Transaction}

local function newTransaction(pack, path, type, contents, version)
	local transaction = {
		pack = pack,
		path = path,
		type = type,
		contents = contents,
		version = version,
		backup = nil,
	}

	setmetatable(transaction, tmeta)

	return transaction
end

local TransQueue = {
	addFile = function(self, path, contents, version)
		table.insert(self.transactions, newTransaction(self.pack, path, "writeFile", contents, version))
	end,
	removeFile = function(self, path)
		table.insert(self.transactions, newTransaction(self.pack, path, "deleteFile"))
	end,
	makeDir = function(self, path)
		if string.match(path, "(.-)/[^/]+$") and not fs.exists(fs.combine(installRoot, string.match(path, "(.-)/[^/]+$"))) then
			self:makeDir(string.match(path, "(.-)/[^/]+$"))
		end
		if not fs.exists(fs.combine(installRoot, path)) then
			table.insert(self.transactions, newTransaction(self.pack, path, "makeDirectory"))
		end
	end,
	removeDir = function(self, path)
		table.insert(self.transactions, newTransaction(self.pack, path, "removeDirectory"))
	end,
	env = function(self)
		return {
			addFile = function(path, contents, version)
				self:addFile(path, contents, version)
			end,
			removeFile = function(path)
				self:removeFile(path)
			end,
			makeDir = function(path)
				self:makeDir(path)
			end,
			removeDir = function(path)
				self:removeDir(path)
			end,
		}
	end,
	finish = function(self)
		local installedFile = fs.combine(fs.combine(installRoot, "/etc/.installed"), self.pack.fullName)
		local installedData = {}
		if installed[self.pack.fullName] and fs.exists(installedFile) then
			local handle = io.open(installedFile, "r")
			if handle then
				for line in handle:lines() do
					table.insert(installedData, line)
				end
				handle:close()

				--strip version number if present
				if #installedData >= 1 then
					table.remove(installedData, 1)
				end
			end
		end

		--ensure installed table entry exists
		if not installed[self.pack.fullName] then
			installed[self.pack.fullName] = {
				version = self.pack.version,
				files = {},
			}
			if not installed[self.pack.name] then installed[self.pack.name] = {[self.pack.repo] = installed[self.pack.fullName]} end
		end

		local fileInfoUpdates = {}

		local lastTransaction = false

		for i = 1, #self.transactions do
			if not self.transactions[i]:finish() then
				--clean up already-processed transactions and exit
				printWarning("Transaction failed! Rolling back...")
				for j = i, 1, -1 do
					self.transactions[i]:rollback()
					return false
				end
			end
			--construct new line (or nil to remove entry, if present)
			local newLine
			if self.transactions[i].type == "writeFile" then
				table.insert(fileInfoUpdates, newTransaction(self.pack, self.transactions[i].path, "updateInfo", installedData, self.transactions[i].version))
			elseif self.transactions[i].type == "makeDirectory" then
				table.insert(fileInfoUpdates, newTransaction(self.pack, self.transactions[i].path, "updateInfo", installedData))
			elseif self.transactions[i].type == "deleteFile" or self.transactions[i].type == "removeDirectory" then
				table.insert(fileInfoUpdates, newTransaction(self.pack, self.transactions[i].path, "removeInfo", installedData))
			end
		end

		--modify installed data to match the transactions succesfully executed.
		printInformation("Updating Database")
		for i = 1, #fileInfoUpdates do
			fileInfoUpdates[i]:finish()
		end

		if self.removing and #installedData == 0 then
			fs.delete(installedFile)

			--remove entries from installed packages table if removing 
			installed[self.pack.name][self.pack.repo] = nil
			local othersWithName = false
			for k, v in pairs(installed[self.pack.name]) do
				if v then
					othersWithName = true
					break
				end
			end
			if not othersWithName then
				installed[self.pack.name] = nil
			end
			installed[self.pack.fullName] = nil
		else
			--write out file again, if any content exists for it.
			table.insert(installedData, 1, tostring(self.pack.version))
			if not fs.exists(fs.combine(fs.combine(installRoot, "/etc/.installed"), self.pack.repo)) then fs.makeDir(fs.combine(fs.combine(installRoot, "/etc/.installed"), self.pack.repo)) end
			local handle = io.open(installedFile, "w")
			if handle then
				for k, v in ipairs(installedData) do
					handle:write(v.."\n")
				end
				handle:close()
			end
		end
		return true
	end,
}

local queueMeta = {__index = TransQueue}

function newTransactionQueue(packName, removing)
	local pack
	if list[packName] and list[packName].version then pack = list[packName] else return nil, "No such package!" end
	local queue = {
		pack = pack,
		removing = removing,
		transactions = {},
	}

	setmetatable(queue, queueMeta)

	return queue
end

local downloadTypes = {
	github = {
		author = true,
		repository = true,
		branch = true,
	},
	bitbucket = {
		author = true,
		repository = true,
		branch = true,
	},
	pastebin = {
		url = true,
		filename = true,
	},
	raw = {
		url = true,
		filename = true,
	},
	multi = {},
	grin = {
		author = true,
		repository = true,
	},
	meta = {},
}

local updateTypes = {
	github = "incremental",
	bitbucket = "incremental",
	grin = "replace",
	raw = "overwrite",
	multi = "overwrite",
	pastebin = "overwrite",
	meta = "overwrite",
}

local lookupFunctions = {}

lookupFunctions.github = function(package)
	local function getDirectoryContents(path)
		local fType, fPath, fVer = {}, {}, {}
		local response = http.get("https://api.github.com/repos/"..download.author.."/"..download.repository.."/contents/"..path.."?ref="..download.branch)
		if response then
			response = response.readAll()
			if response ~= nil then
				for str in response:gmatch('"type":%s*"(%w+)",') do table.insert(fType, str) end
				for str in response:gmatch('"path":%s*"([^\"]+)",') do table.insert(fPath, str) end
				for str in response:gmatch('"sha":%s*"([^\"]+)",') do table.insert(fVer, str) end
			end
		else
			printWarning("Can't fetch repository information")
			return nil
		end
		local directoryContents = {}
		for i=1, #fType do
			directoryContents[i] = {type = fType[i], path = fPath[i], version = fVer[i]}
		end
		return directoryContents
	end
	local function addDirectoryContents(path, contentsTable)
		local contents = getDirectoryContents(path)
		if not contents then return nil, "no contents" end
		for n, file in ipairs(contents) do
			if file.type == "dir" then
				addDirectoryContents(file.path, contentsTable)
			else
				table.insert(contentsTable, {path = file.path, version = file.version})
			end
		end
		return contentsTable
	end
	return addDirectoryContents("", {})
end

lookupFunctions.bitbucket = function(package)
	local function getDirectoryContents(path)
		local directoryContents = {}
		local response = http.get("https://api.bitbucket.org/1.0/repositories/"..download.author.."/"..download.repository.."/src/"..download.branch..path)
		if response then
			response = response.readAll()
			if response ~= nil then
				for str in string.gmatch(string.match(response, '"directories": %[(.-)%]'), '"([^,\"]+)"') do table.insert(directoryContents, {type = "dir", path = str}) end
				for str, ver in string.gmatch(string.match(response, '"files": %[(.-)%]'), '"path": "([^\"]+)".-"revision": "([^\"]+)"') do table.insert(directoryContents, {type = "file", path = str, version = ver}) end
			end
		else
			printWarning("Can't fetch repository information")
			return nil
		end
		return directoryContents
	end
	local function addDirectoryContents(path, contentsTable)
		local contents = getDirectoryContents(path)
		for n, file in ipairs(contents) do
			if file.type == "dir" then
				addDirectoryContents(path..file.path.."/", contentsTable)
			else
				table.insert(contentsTable, {path = file.path, version = file.version})
			end
		end
		return contentsTable
	end
	return addDirectoryContents("/", {})
end

-- Local function to download a url raw
local function raw(url)
	printInformation("Fetching: "..url)
	http.request(url)
	while true do
		local event = {os.pullEvent()}
		if event[1] == "http_success" then
			printInformation("Done!")
			return event[3].readAll()
		elseif event[1] == "http_failure" then
			printWarning("Unable to fetch file "..event[2])
			return false
		end
	end
end

local downloadFunctions = {}

downloadFunctions.raw = function(pack, env, queue)
	-- Delegate to local raw
	local path = fs.combine(pack.target, pack.download.filename)

	if string.match(path, "(.-)/[^/]+$") then
		queue:makeDir(string.match(path, "(.-)/[^/]+$"))
	end
	local content = raw(pack.download.url)
	if content then
		queue:addFile(path, content)
		return true
	else
		return false
	end
end

downloadFunctions.multi = function(pack, env, queue)
	local files = pack.download.files
	for i = 1, #files do
		local path = fs.combine(pack.target, files[i].name)

		if string.match(path, "(.-)/[^/]+$") then
			queue:makeDir(string.match(path, "(.-)/[^/]+$"))
		end
		local content = raw(files[i].url)
		if content then
			queue:addFile(path, content)
		else
			return false
		end
	end
	return true
end

downloadFunctions.github = function(pack, env, queue)
	local contents = lookupFunctions.github(pack)
	if not contents then return nil, "content fetch failure" end
	local localTarget = pack.target or ""
	for num, file in ipairs(contents) do
		local path = fs.combine(localTarget, file.path)
		if string.match(path, "(.-)/[^/]+$") then
			queue:makeDir(string.match(path, "(.-)/[^/]+$"))
		end
		local content = raw("https://raw.github.com/"..pack.download.author.."/"..pack.download.repository.."/"..pack.download.branch.."/"..file.path)
		if content then
			queue:addFile(path, content, file.version)
		else
			return false
		end
	end
	return true
end

downloadFunctions.bitbucket = function(pack, env, queue)
	local contents = lookupFunctions.bitbucket(pack)
	local localTarget = pack.target or ""
	for num, file in ipairs(contents) do
		local path = fs.combine(localTarget, file.path)
		if string.match(path, "(.-)/[^/]+$") then
			queue:makeDir(string.match(path, "(.-)/[^/]+$"))
		end
		local content = raw("https://bitbucket.org/"..pack.download.author.."/"..pack.download.repository.."/raw/"..pack.download.branch.."/"..file.path)
		if content then
			queue:addFile(path, content, file.version)
		else
			return false
		end
	end
	return true
end

downloadFunctions.pastebin = function(pack, env, queue)
	local path = fs.combine(pack.target, pack.download.filename) 

	if string.match(path, "(.-)/[^/]+$") then
		queue:makeDir(string.match(path, "(.-)/[^/]+$"))
	end
	local content = raw("http://pastebin.com/raw.php?i="..pack.download.url)
	if content then
		queue:addFile(path, content)
		return true
	else
		return false
	end
end

downloadFunctions.grin = function(pack, env, queue)
	local fullName = pack.repo.."/"..pack.name
	local status
	parallel.waitForAny(function()
		status = env.shell.run("pastebin run VuBNx3va -e -u", pack.download.author, "-r", pack.download.repository, fs.combine(fs.combine(installRoot, pack.target), pack.name))
	end, function()
		while true do
			local e, msg = os.pullEvent("grin_install_status")
			printInformation(msg)
		end
	end)
	return status
end

downloadFunctions.meta = function(pack, env, queue)
	return true
end

local function findInstalledVersionByPath(packName, path)
	for i, file in ipairs(installed[packName].files) do
		if file.path == path then return file.version end
	end
end

local new_fs = {}

local Package = {
	install = function(self, env)
		local queue
		if downloadFunctions[self.download.type] then
			queue = newTransactionQueue(self.fullName)
			if not downloadFunctions[self.download.type](self, env, queue) then return false end
		else
			return false
		end

		if not queue:finish() then return false end

		--execute startup script, if present.
		if self.setup then
			local queue = newTransactionQueue(self.fullName)
			--packman key included solely for backwards compatibility, usage is deprecated in favor of pack.
			local setupArgs = {}
			for match in string.gmatch(self.setup, "(%S+)") do
				table.insert(setupArgs, match)
			end
			setupArgs[1] = fs.combine(fs.combine(installRoot, self.target), setupArgs[1])
			if not os.run({shell = env.shell, packman = queue:env(), pack = queue:env(), fs = new_fs}, unpack(setupArgs)) then
				--setup script threw an error.
				printWarning("Package "..self.fullName.." failed to install, removing")
				return self:remove(env)
			end

			--this must be done a second time to finalize any changes made by the install script.
			return queue:finish()
		end
		return true
	end,
	remove = function(self, env)
		if not installed[self.fullName] then return false end
		local queue = newTransactionQueue(self.fullName, true)

		if self.cleanup then
			local queue = newTransactionQueue(self.fullName, true)
			local cleanupArgs = {}
			for match in string.gmatch(self.cleanup, "(%S+)") do
				table.insert(cleanupArgs, match)
			end
			cleanupArgs[1] = fs.combine(fs.combine(installRoot, self.target), cleanupArgs[1])
			os.run({shell = env.shell, packman = queue:env(), pack = queue:env(), fs = new_fs}, unpack(cleanupArgs))
			if not queue:finish() then return false end
		end

		local fileList = installed[self.fullName].files
		for i = #fileList, 1, -1 do
			if fs.exists(fs.combine(installRoot, fileList[i].path)) and fs.isDir(fs.combine(installRoot, fileList[i].path)) then
				queue:removeDir(fileList[i].path)
			else
				queue:removeFile(fileList[i].path)
			end

		end

		return queue:finish()
	end,
	upgrade = function(self, env)
		if not installed[self.fullName] then return false end
		local queue = newTransactionQueue(self.fullName)
		if updateTypes[self.download.type] == "incremental" then
			local updatedFiles = {}
			local contents = lookupFunctions[self.download.type](self)
			for num, file in ipairs(contents) do
				local path = fs.combine(self.target, file.path)
				if file.version ~= findInstalledVersionByPath(self.fullName, path) then
					if string.match(path, "(.-)/[^/]+$") then
						queue:makeDir(string.match(path, "(.-)/[^/]+$"))
					end
					if self.download.type == "github" then
						local content = raw("https://raw.github.com/"..self.download.author.."/"..self.download.repository.."/"..self.download.branch.."/"..file.path)
						if content then
							queue:addFile(path, content, file.version)
						else
							return false
						end
					elseif self.download.type == "bitbucket" then
						local content = raw("https://bitbucket.org/"..self.download.author.."/"..self.download.repository.."/raw/"..self.download.branch.."/"..file.path)
						if content then
							queue:addFile(path, content, file.version)
						else
							return false
						end
					end
				end
				updatedFiles[path] = true
			end

			for i, fileInfo in ipairs(installed[self.fullName].files) do
				if not updatedFiles[fileInfo.path] and fileInfo.version ~= installed[self.fullName].version then
					if not fs.isDir(fs.combine(installRoot, fileInfo.path)) or (fs.isDir(fs.combine(installRoot, fileInfo.path)) and #(fs.list(fs.combine(installRoot, fileInfo.path))) == 0) then
						queue:removeFile(fileInfo.path)
					end
				end
			end

			
			return queue:finish()
		elseif updateTypes[self.download.type] == "overwrite" then
			if not downloadFunctions[self.download.type](self, env, queue) then return false end
			return queue:finish()
		elseif updateTypes[self.download.type] == "replace" then
			return self:remove(env) and self:install(env)
		end
	end,
}

local pmetatable = {__index = Package}

function new(name, repo)
	local p = {
		name = name,
		repo = repo,
		fullName = repo.."/"..name,
		version = "",
		size = 0,
		category = {},
		dependencies = {},
		--installation folder target
		target = "/bin",
		setup = nil,
		remove = nil,
		download = {}
	}

	setmetatable(p, pmetatable)

	return p
end

function findDependencies(packageName, _dependencyTable)
	local dependencyTable = _dependencyTable or {}
	if list[packageName] then
		dependencyTable[packageName] = true
		for packName in pairs(list[packageName].dependencies) do
			packName = packName:lower()
			if packName ~= "none" and not dependencyTable[packName] then
				dependencyTable, errmsg = findDependencies(packName, dependencyTable)
				if not dependencyTable then return nil, errmsg end
			end
		end
	else
		return nil, packageName
	end
	return dependencyTable
end

if not fs.exists("/bin") then fs.makeDir("/bin") end
--process package list
local function addPacks(file)
	local packName = fs.getName(file)
	local state = ""
	local listHandle = io.open(file, "r")
	local entryTable
	local lineCount = 1
	if listHandle then
		for line in listHandle:lines() do
			if state == "type" then
				local allAttributes = true
				for attribute in pairs(downloadTypes[entryTable.download.type]) do
					if not entryTable.download[attribute] then
						allAttributes = false
						break
					end
				end
				if allAttributes then
					state = "main"
				end
			end
			local property,hasValue,value = string.match(line, "^%s*([^=%s]+)%s*(=?)%s*(.-)%s*$")
			hasValue=hasValue~="" or nil
			if property == "name" and state == "" then
				if state == "" then
					entryTable = new(string.lower(value), packName)
					entryTable.target = "/bin"
					state = "main"
				else
					if state ~= "dirty" then
						printWarning("Unexpected 'name' at line "..lineCount.." in "..file)
						state = "dirty"
					end
				end
			elseif property == "type" then
				if state == "main" then
					entryTable.download.type = string.match(value, "^(%S*)$")
					if downloadFunctions[entryTable.download.type] then
						if entryTable.download.type == "multi" then
							entryTable.download.files = {}
							state = "main"
						else
							state = "type"
						end
					else
						if state ~= "dirty" then
							printWarning("Unknown Repository Format at line "..lineCount.." in "..file)
							state = "dirty"
						end
					end
				else
					if state ~= "dirty" then
						printWarning("Unexpected 'type' at line "..lineCount.." in "..file)
						state = "dirty"
					end
				end
			elseif property == "file" then
				if entryTable.download.type == "multi" then
					local fileTable = entryTable.download.files
					local name, url = string.match(value, "(%S+)%s+(.*)")
					fileTable[#fileTable + 1] = {name = name, url = url}
				else
					printWarning("Unexpected "..property.." at line "..lineCount.." in "..file)
					state = "dirty"
				end
			elseif property == "target" or property == "setup" or property == "update" or property == "cleanup" or property == "version" or property == "size" then
				if state == "main" then
					entryTable[property] = value
				else
					if state ~= "dirty" then
						printWarning("Unexpected "..property.." at line "..lineCount.." in "..file)
						state = "dirty"
					end
				end
			elseif property == "dependencies" or property == "category" then
				if state == "main" then
					for str in string.gmatch(value, "(%S+)") do
						entryTable[property][str] = true
					end
				else
					if state ~= "dirty" then
						printWarning("Unexpected "..property.." at line "..lineCount.." in "..file)
						state = "dirty"
					end
				end
			elseif property == "end" then
				if state == "dirty" then
					state = ""
				elseif state == "type" then
					printWarning("Unexpected end at line "..lineCount.." in "..file)
					state = ""
				elseif state == "main" then
					--this line is the required entries for a valid repolist entry.
					if entryTable.download.type and #entryTable.version > 0 and (tonumber(entryTable.size) > 0 or entryTable.download.type == "meta") then
						local i
						for name in pairs(entryTable.dependencies) do
							i = true
							break
						end
						if i then
							list[packName.."/"..entryTable.name] = entryTable
							if list[entryTable.name] then
								list[entryTable.name][packName] = entryTable
							else
								list[entryTable.name] = {[packName] = entryTable}
							end
						end
					else
						entryTable = nil
					end
					state = ""
				end
			elseif state == "type" then
				local propertyFound = false
				for prop in pairs(downloadTypes[entryTable.download.type]) do
					if property == prop then
						propertyFound = true
						break
					end
				end
				if propertyFound then
					entryTable.download[property] = value
				else
					printWarning("Unexpected "..property.." at line "..lineCount.." in "..file)
					state = "dirty"
				end
			end
			lineCount = lineCount + 1
		end
		if state ~= "" then
			printWarning("Expected 'end' at line "..lineCount.." in "..file)
		end
		listHandle:close()
	else
		printError("Could not open repository list!")
	end
end

function load()
	for k, v in pairs(list) do
		list[k] = nil
	end
	if fs.exists("/etc/repositories") then
		for _, file in ipairs(fs.list("/etc/repositories")) do
			addPacks(fs.combine("/etc/repositories", file))
		end
	end

	for k, v in pairs(installed) do
		installed[k] = nil
	end
	if fs.exists(fs.combine(installRoot, "/etc/.installed")) and fs.isDir(fs.combine(installRoot, "/etc/.installed")) then
		for _, repo in ipairs(fs.list(fs.combine(installRoot, "/etc/.installed"))) do
			for _, file in ipairs(fs.list(fs.combine(fs.combine(installRoot, "/etc/.installed"), repo))) do
				local name = repo.."/"..file
				local handle = io.open(fs.combine(fs.combine(fs.combine(installRoot, "/etc/.installed"), repo), file), "r")
				if handle then
					installed[name] = {files = {}}
					local packVersion
					for line in handle:lines() do
						if not packVersion then
							packVersion = line
							installed[name].version = packVersion
						else
							local path, version = string.match(line, "([^;]+);(.*)")
							if path and version then
								installed[name].files[#installed[name].files + 1] = {path = path, version = version}
							else
								installed[name].files[#installed[name].files + 1] = {path = line, version = packVersion}
							end
						end
					end
					handle:close()
					if installed[file] then
						installed[file][repo] = installed[name]
					else
						installed[file] = {[repo] = installed[name]}
					end
				else
					printWarning("Couldn't open package db file: "..file)
				end
			end
		end
	end

	for k, v in pairs(new_fs) do
		new_fs[k] = nil
	end
	do
		local root = installRoot
		--override fs api to use installRoot, recreated when loading to accomodate installRoot changes.
		local function fsWrap(name,f,n)
			return function(...)
				local args = { ... }
				for k,v in ipairs(args) do
					if n == nil or k <= n then
						args[k] = fs.combine(root, v)
					end
				end
				return f(unpack(args))
			end
		end
		for k,v in pairs(fs) do
			new_fs[k] = fsWrap(k,v,nil)
		end
		new_fs.open = fsWrap("open",fs.open,1)
		new_fs.combine = fs.combine
		new_fs.getName = fs.getName
		new_fs.getDir = fs.getDir
	end
end

local versionNumber = 1.3
local unpack = unpack or table.unpack


local args = {...}

local opwords = {
	install = true,
	remove = true,
	update = true,
	list = true,
	search = true,
}

local argwords = {
	fetch = true,
	force = true,
	target = 1,
}

if #args < 1 or (not opwords[args[1]] and not argwords[args[1]]) then
	io.write("Usage:\n")
	io.write("packman [options] install <package name[s]>\n")
	io.write("packman [options] update <package name[s]>\n")
	io.write("packman [options] remove <package name[s]>\n")
	io.write("packman [options] list [pattern]\n")
	io.write("packman [options] search [pattern]\n")
	io.write("\n")
	io.write("Options:\n")
	io.write("fetch\n")
	io.write("    Update repository and package lists before performing operations (can be used without an operation)\n")
	io.write("force\n")
	io.write("    Force yes answers when manipulating packages\n")
	io.write("target <directory>\n")
	io.write("    Set root directory to install packages in\n")
	return
end

local mode = ""
local forced = false
local target = "/"
local fetch = false
local argState = nil
local argCount = 0
local operation = {options = {}, arguments = {}}

--lower all arguments
for i = 1, #args do
	args[i] = string.lower(args[i])
	if argState == nil and args[i] == "fetch" then fetch = true end
	if argState == nil and args[i] == "force" then forced = true end

	if argwords[args[i]] and type(argwords[args[i]]) == "number" then
		operation.options[args[i]] = {}
		argState = args[i]
		argCount = argwords[args[i]]
	elseif opwords[args[i]] then
		mode = args[i]
		argState = "arguments"
		argCount = 0
	elseif argState and argCount > 0 then
		--option arguments
		table.insert(operation.options[argState], args[i])
		argCount = argCount - 1
		if argCount == 0 then argState = nil end
	elseif argState == "arguments" then
		--operation arguments
		table.insert(operation.arguments, args[i])
	end
end

if operation.options.target then
	target = operation.options.target[1]
end

local function resetScreen()
	term.setTextColor(colors.white)
	term.setBackgroundColor(colors.black)
end

local function printError(errorText)
	if term.isColor() then term.setTextColor(colors.red) end
	io.write(errorText.."\n")
	term.setTextColor(colors.white)
	error()
end

local function printWarning(warningText)
	if term.isColor() then term.setTextColor(colors.yellow) end
	io.write(warningText.."\n")
	term.setTextColor(colors.white)
end

local function printInformation(infoText)
	if term.isColor() then term.setTextColor(colors.lime) end
	io.write(infoText.."\n")
	term.setTextColor(colors.white)
end

local function loadPackageAPI()
	if not package then if shell.resolveProgram("package") then os.loadAPI(shell.resolveProgram("package")) elseif fs.exists("etc/api/package") then os.loadAPI("etc/api/package") elseif not fetch then error("Could not load package API!") end end

	if package then
		resetScreen()
		io.write("Loading database...\n")
		installRoot = target

		local co = coroutine.create(load)
		local event, filter, passback = {}
		while true do
			if (filter and (filter == event[1] or event[1] == "terminate")) or not filter then
				passback = {coroutine.resume(co, unpack(event))}
			end
			if passback[1] == false then printWarning(passback[2]) end
			if coroutine.status(co) == "dead" then break end
			filter = nil
			if passback and passback[1] and passback[2] then
				filter = passback[2]
			end
			event = {os.pullEventRaw()}
			if event[1] == "package_status" then
				if event[2] == "info" then
					printInformation(event[3])
				elseif event[2] == "warning" then
					printWarning(event[3])
				elseif event[2] == "error" then
					printError(event[3])
				end
			end
		end
	end
end

loadPackageAPI()

local categoryList = {}
local categorySorted = {}

if fetch then
	local queue
	if package and not pack then
		queue = newTransactionQueue("main/packman")
	end
	io.write("Updating packman\n")
	local remoteHandle = http.get("https://raw.github.com/Piorjade/cLinux/master/bin/packman")
	if remoteHandle then
		if pack then
			pack.addFile(shell.getRunningProgram(), remoteHandle.readAll())
		elseif queue then
			queue:addFile(shell.getRunningProgram(), remoteHandle.readAll())
		else
			local fileHandle = io.open(shell.getRunningProgram(), "w")
			if fileHandle then
				fileHandle:write(remoteHandle.readAll())
				fileHandle:close()
			else
				printWarning("Could not write file "..shell.getRunningProgram())
			end
		end
		remoteHandle.close()
	else
		printWarning("Could not retrieve remote file.")
	end
	io.write("Updating package API\n")
	remoteHandle = http.get("https://raw.github.com/Piorjade/cLinux/master/etc/api/package")
	if remoteHandle then
		if pack then
			pack.makeDir("/etc/api")
			pack.addFile("/etc/api/package", remoteHandle.readAll())
		elseif queue then
			queue:makeDir("/etc/api")
			queue:addFile("/etc/api/package", remoteHandle.readAll())
		else
			if not fs.exists("/etc/api") then fs.makeDir("/etc/api") end
			local fileHandle = io.open("/etc/api/package", "w")
			if fileHandle then
				fileHandle:write(remoteHandle.readAll())
				fileHandle:close()
			else
				printWarning("Could not write file /etc/api/package")
			end
		end
		remoteHandle.close()
	else
		printWarning("Could not retrieve remote file.")
	end
	io.write("Fetching Repository List\n")
	remoteHandle = http.get("http://pastebin.com/raw/m8wdq6Vn")
	if remoteHandle then
		if pack then
			pack.makeDir("/etc")
			pack.addFile("/etc/repolist", remoteHandle.readAll())
		elseif queue then
			queue:makeDir("/etc")
			queue:addFile("/etc/repolist", remoteHandle.readAll())
		else
			local fileHandle = io.open("/etc/repolist", "w")
			if fileHandle then
				fileHandle:write(remoteHandle.readAll())
				fileHandle:close()
			else
				printWarning("Could not write file /etc/repolist")
			end
		end
		remoteHandle.close()
	else
		printWarning("could not retrieve remote file.")
	end
	if fs.exists("/etc/repolist") then
		if pack then
			pack.makeDir("/etc/repositories")
		elseif queue then
			queue:makeDir("/etc/repositories")
		else
			if not fs.exists("/etc/repositories") then fs.makeDir("/etc/repositories") end
		end

		local handle = io.open("/etc/repolist", "r")
		if handle then
			for line in handle:lines() do
				local file, url = string.match(line, "^(%S*)%s*(.*)")
				if file and url then
					io.write("Fetching Repository: "..file.."\n")
					local remoteHandle = http.get(url)
					if remoteHandle then
						if pack then
							pack.addFile(fs.combine("/etc/repositories", file), remoteHandle.readAll())
						elseif queue then
							queue:addFile(fs.combine("/etc/repositories", file), remoteHandle.readAll())
						else
							local fileHandle = io.open(fs.combine("/etc/repositories", file), "w")
							if fileHandle then
								fileHandle:write(remoteHandle.readAll())
								fileHandle:close()
							else
								printWarning("Could not write file: "..fs.combine("/etc/repositories", file))
							end
						end
						remoteHandle.close()
					else
						printWarning("Could not retrieve remote file: "..file)
					end
				end
			end
		else
			printError("Failed to open repository list")
		end
	end

	if queue then
		queue:finish()
	end

	fetch = false

	if #mode > 0 then
		--reload package API.
		os.unloadAPI("package")
		loadPackageAPI()
	end
end

if #mode > 0 then
	for n, v in pairs(list) do
		if v.category then
			for category in pairs(v.category) do
				if not categoryList[category] then
					categoryList[category] = {[n] = true}
					table.insert(categorySorted, category)
				else
					categoryList[category][n] = true
				end
			end
		end
	end
	table.sort(categorySorted)

	local badPackages = {}
	--flesh out dependencies
	for pName, pData in pairs(list) do
		if pData.dependencies then
			dependencies, errmsg = findDependencies(pName, {})
			if not dependencies then
				--if dependencies could not be resolved, remove the 
				printWarning("Could not resolve dependency on "..errmsg.." in package "..pName)
				table.insert(badPackages, pName)
			else
				pData.dependencies = dependencies
			end
		end
	end
	--actual package removal and short-name lookup cleanup.
	for _, pack in pairs(badPackages) do
		local entry = list[pack]
		local name = entry.name
		local others, key = false
		for k, v in pairs(list[name]) do
			if v == entry then
				key = k
			else
				others = true
			end
		end
		if others then
			list[name][key] = nil
		else
			list[name] = nil
		end
		list[pack] = nil
	end
end

local function lookupPackage(name, installedOnly)
	if list[name] and not list[name].dependencies then
		local options = {}
		if installedOnly and installed[name] then
			for name, pack in pairs(installed[name]) do
				table.insert(options, name)
			end
		elseif installedOnly then
			--using installedOnly, but no packages of that name are installed.
			return false
		else
			for name, pack in pairs(list[name]) do
				table.insert(options, name)
			end
		end
		if #options > 1 then
			io.write("Package "..name.." is ambiguous.\n")
			for i = 1, #options do
				write(tostring(i)..": "..options[i].."  ")
			end
			io.write("\n")
			io.write("Select option: \n")
			local selection = io.read()
			if tonumber(selection) and options[tonumber(selection)] then
				return options[tonumber(selection)].."/"..name
			end
		elseif #options == 1 then
			return options[1].."/"..name
		else
			return false
		end
	elseif list[name] then
		--since it must have a dependencies table, the name is already fully unique.
		return name
	else
		return false
	end
end

local function raw_package_operation(name, funcName)
	local pack = list[name]
	if not pack then return nil, "No such package" end
	local co = coroutine.create(function() return pack[funcName](pack, getfenv()) end)
	local event, filter, passback = {}
	while true do
		if (filter and (filter == event[1] or event[1] == "terminate")) or not filter then
			passback = {coroutine.resume(co, unpack(event))}
		end
		if passback[1] == false then printWarning(passback[2]) end
		if coroutine.status(co) == "dead" then return unpack(passback, 2) end
		filter = nil
		if passback and passback[1] and passback[2] then
			filter = passback[2]
		end
		event = {os.pullEventRaw()}
		if event[1] == "package_status" then
			if event[2] == "info" then
				printInformation(event[3])
			elseif event[2] == "warning" then
				printWarning(event[3])
			elseif event[2] == "error" then
				printError(event[3])
			end
		end
	end
end

local function install(name)
	return raw_package_operation(name, "install")
end

local function remove(name)
	return raw_package_operation(name, "remove")
end

local function upgrade(name)
	return raw_package_operation(name, "upgrade")
end

if mode == "install" then
	if #operation.arguments >= 1 then
		local installList = {}
		for packageNumber, packageName in ipairs(operation.arguments) do
			local result = lookupPackage(packageName)
			if not result then
				printWarning("Could not install package "..packageName..".")
			else
				for k,v in pairs(list[result].dependencies) do
					if not installed[k] then
						installList[k] = true
					else
						if k == result then
							printInformation("Package "..k.." already installed")
						else
							printInformation("Dependency "..k.." already installed")
						end
					end
				end
			end
		end
		local installString = ""
		for k, v in pairs(installList) do
			installString = installString..k.." "
		end
		if #installString > 0 then
			if not forced then
				io.write("The following packages will be installed: "..installString.."\n")
				io.write("Continue? (Y/n)\n")
				local input = io.read()
				if string.sub(input:lower(), 1, 1) == "n" then
					return true
				end
			end
			for packageName in pairs(installList) do
				if not install(packageName) then
					printWarning("Could not "..mode.." package "..packageName)
				end
			end
		end
	end
elseif mode == "update" then
	local updateList = {}
	local installList = {}
	if #operation.arguments >= 1 then
		for _, name in ipairs(operation.arguments) do
			local result = lookupPackage(name, true)
			if result then
				table.insert(updateList, result)
			end
		end
	else
		for k, v in pairs(installed) do
			if v.files then
				--filters out the disambiguation entries.
				table.insert(updateList, k)
				for name, info in pairs(list[k].dependencies) do
					if not installed[name] then
						installList[name] = true
					end
				end
			end
		end
	end
	local installString = ""
	for k, v in pairs(installList) do
		installString = installString..k.." "
	end
	if not forced then
		for i = #updateList, 1, -1 do
			if installed[updateList[i]].version == list[updateList[i]].version then
				table.remove(updateList, i)
			end
		end
	end
	if #updateList > 0 or #installString > 0 then
		local updateString = ""
		for i = 1, #updateList do
			updateString = updateString..updateList[i].." "
		end
		if not forced then
			io.write("The following packages will be updated: "..updateString.."\n")
			if #installString > 0 then
				io.write("The following packages will also be installed: "..installString.."\n")
			end
			io.write("Continue? (Y/n)\n")
			local input = io.read()
			if string.sub(input:lower(), 1, 1) == "n" then
				return true
			end
		end
		local failureCount = 0
		for packageName in pairs(installList) do
			if not install(packageName) then
				printWarning("Could not install package "..packageName)
			end
		end
		for _, packageName in pairs(updateList) do
			if not upgrade(packageName) then
				printWarning("Package "..packageName.." failed to update.")
				failureCount = failureCount + 1
			end
		end
		if failureCount > 0 then
			printWarning(failureCount.." packages failed to update.")
		else
			printInformation("Update complete!")
		end
	else
		io.write("Nothing to do!\n")
		return true
	end
elseif mode == "remove" then
	if #operation.arguments >= 1 then
		local packageList = {}
		for _, name in ipairs(operation.arguments) do
			local result = lookupPackage(name, true)
			if result then
				table.insert(packageList, result)
			end
		end
		dependeesList = {}
		--find packages which depend on the packages we are removing.
		for pName, pData in pairs(installed) do
			if pData.version then
				if not packageList[pName] then
					for dName in pairs(list[pName].dependencies) do
						for _, packName in pairs(packageList) do
							if packName == dName then
								dependeesList[pName] = true
								break
							end
						end
						if dependeesList[pName] then
							break
						end
					end
				end
			end
		end
		local removeString = ""
		local dependeesString = ""
		for i = 1, #packageList do
			removeString = removeString..packageList[i].." "
			if dependeesList[packageList[i]] then
				dependeesList[packageList[i]] = nil
			end
		end
		for dName in pairs(dependeesList) do
			dependeesString = dependeesString..dName.." "
		end
		if #removeString > 0 then
			if not forced then
				io.write("The following packages will be removed: "..removeString.."\n")
				if #dependeesString > 0 then
					io.write("The following packages will also be removed due to missing dependencies: "..dependeesString.."\n")
				end
				io.write("Continue? (y/N)\n")
				local input = io.read()
				if string.sub(input:lower(), 1, 1) ~= "y" then
					return true
				end
			end
			for pName in pairs(dependeesList) do
				printInformation("Removing "..pName)
				remove(pName)
			end
			for _, pName in pairs(packageList) do
				printInformation("Removing "..pName)
				remove(pName)
			end
		else
			io.write("Nothing to do!\n")
		end
	end
elseif mode == "list" then
	--list all installed packages
	local match = ".*"
	if #operation.arguments == 1 then
		--list with matching.
		match = operation.arguments[1]
	end
	for name, info in pairs(installed) do
		if info.version then
			if string.match(name, match) then
				io.write(name.." "..info.version.."\n")
			end
		end
	end
elseif mode == "search" then
	--search all available packages
	local match = ".*"
	if #operation.arguments == 1 then
		--search using a match
		match = operation.arguments[1]
	end
	for name, info in pairs(list) do
		if info.version then
			if string.match(name, match) then
				io.write((installed[name] and "I " or "A " )..name.." "..info.version.."\n")
			end
		end
	end
end