local Util       = require('opus.util')

local device     = _G.device
local kernel     = _G.kernel
local os         = _G.os

local containers = {
	manipulator = true,
	neuralInterface = true,
}

local cache = { }

-- manipulators will throw an error on listModules
-- if the user has logged off
local function getModules(dev, side)
	local list = { }
	local s, m = pcall(function()
		if dev and dev.listModules then
			for _, module in pairs(dev.listModules()) do
				list[module] = Util.shallowCopy(dev)
				list[module].name = module
				list[module].type = module
				list[module].side = side
			end
		end
	end)
	if not s and m then
		_G._syslog(m)
	end
	return list
end

-- if a device has been reattached, reuse the existing
-- table so any references to the table are retained
local function addDevice(dev, args, doQueue)
	local name = args.name

	if not cache[name] then
		cache[name] = { }
	end
	device[name] = cache[name]
	Util.merge(device[name], dev)
	Util.merge(device[name], args)

	if doQueue then
		os.queueEvent('device_attach', name)
	end
end

-- directly access the peripheral as the methods in getInventory, etc.
-- can become invalid without any way to tell
local function damnManipulator(container, method, args, doQueue)
	local dev = { }
	local methods = {
		'drop', 'getDocs', 'getItem', 'getItemMeta', 'getTransferLocations',
		'list', 'pullItems', 'pushItems', 'size', 'suck',
	}
	-- the user might not be logged in when the compputer is started
	-- and there's no way to know when they have logged in.
	-- these methods will error if the user is not logged in
	if container[method] then
		for _,k in pairs(methods) do
			dev[k] = function(...)
				return device[container.name][method]()[k](...)
			end
		end

		addDevice(dev, args, doQueue)
	end
end

local function addContainer(v, doQueue)
	-- add devices like plethora:scanner
	for name, dev in pairs(getModules(v, v.side)) do
		-- neural and attached modules have precedence over manipulator modules
		if not device[name] or v.type ~= 'manipulator' then
			addDevice(dev, { name = dev.name, type = dev.name, side = dev.side }, doQueue)
		end
	end

	if v.getName then
		local s, m = pcall(function()
			local name = v.getName()
			if name then
				damnManipulator(v, 'getInventory', {
					name = name .. ':inventory',
					type = 'inventory',
					side = v.side
				}, doQueue)
				damnManipulator(v, 'getEquipment', {
					name = name .. ':equipment',
					type = 'equipment',
					side = v.side
				}, doQueue)
				damnManipulator(v, 'getEnder', {
					name = name .. ':enderChest',
					type = 'enderChest',
					side = v.side
				}, doQueue)
			end
		end)
		if not s and m then
			_G._syslog(m)
		end
	end
end

for k,v in pairs(device) do
	if containers[v.type] then
		cache[k] = v
		addContainer(v)
	end
end

-- register modules as peripherals
kernel.hook('device_attach', function(_, eventData)
	local name = eventData[1]
	local dev = device[name]

	if dev and containers[dev.type] then
		-- so... basically, if you get a handle to device.neuralInterface
		-- (or manipulator) - that handle will still be valid after
		-- a module is removed
		if cache[name] then
			device[name] = cache[name]
			for k,v in pairs(device[name]) do
				if type(v) == 'function' then
					device[name][k] = nil
				end
			end
			Util.merge(device[name], dev)
		else
			cache[name] = dev
		end
		addContainer(dev, true)
	end
end)