local lib = starlit.mod.lib

local suitStore = starlit.store.suitMeta
starlit.item.suit = lib.registry.mk 'starlit:suits';

-- note that this cannot be persisted as a reference to a particular suit in the world
local function suitContainer(stack, inv)
	return starlit.item.container(stack, inv, {
		pfx = 'starlit_suit'
	})
end
starlit.type.suit = lib.class {
	name = 'starlit:suit';
	construct = function(stack)
		return {
			item = stack;
			inv = suitStore(stack);
		}
	end;
	__index = {
		powerState = function(self)
			local s = self.item
			if not s then return nil end
			local m = s:get_meta():get_int('starlit:power_mode')
			if m == 1 then return 'on'
			elseif m == 2 then return 'powerSave'
			else return 'off' end
		end;
		powerStateSet = function(self, state)
			local s = self.item
			if not s then return nil end
			local m
			if state == 'on' then m = 1 -- TODO check power level
			elseif state == 'powerSave' then m = 2
			else m = 0 end
			if self:powerLeft() <= 0 then m = 0 end
			s:get_meta():set_int('starlit:power_mode', m)
		end;
		powerLeft = function(self)
			local batteries = self.inv.read 'batteries'
			local power = 0
			for idx, slot in pairs(batteries) do
				power = power + starlit.mod.electronics.dynamo.totalPower(slot)
			end
			return power
		end;
		powerCapacity = function(self)
			local batteries = self.inv.read 'batteries'
			local power = 0
			for idx, slot in pairs(batteries) do
				power = power + starlit.mod.electronics.dynamo.initialPower(slot)
			end
			return power
		end;
		maxPowerUse = function(self)
			local batteries = self.inv.read 'batteries'
			local w = 0
			for idx, slot in pairs(batteries) do
				w = w + starlit.mod.electronics.dynamo.dischargeRate(slot)
			end
			return w
		end;
		onReconfigure = function(self, inv)
			-- apply any changes to item metadata and export any subinventories
			-- to the provided invref, as they may have changed
			local sc = starlit.item.container(self.item, inv, {pfx = 'starlit_suit'})
			sc:push()
-- 			self:pullCanisters(inv)
		end;
		onItemMove = function(self, user, list, act, what)
			-- called when the suit inventory is changed
			if act == 'put' then
				if list == 'starlit_suit_bat' then
					user:suitSound('starlit-suit-battery-in')
				elseif list == 'starlit_suit_chips' then
					user:suitSound('starlit-suit-chip-in')
				elseif list == 'starlit_suit_canisters' then
					user:suitSound('starlit-insert-snap')
				end
			elseif act == 'take' then
				if list == 'starlit_suit_bat' then
					user:suitSound('starlit-insert-snap')
				elseif list == 'starlit_suit_chips' then
					--user:suitSound('starlit-suit-chip-out')
				elseif list == 'starlit_suit_canisters' then
					user:suitSound('starlit-insert-snap')
				end
			end
		end;
		def = function(self)
			return self.item:get_definition()._starlit.suit
		end;
		--[[
		pullCanisters = function(self, inv)
			starlit.item.container.dropPrefix(inv, 'starlit_canister')
			self:forCanisters(inv, function(sc) sc:pull() end)
		end;
		pushCanisters = function(self, inv, st, i)
			self:forCanisters(inv, function(sc)
				sc:push()
				return true
			end)
		end;
		forCanisters = function(self, inv, fn)
			local cans = inv:get_list 'starlit_suit_canisters'
			if cans and next(cans) then for i, st in ipairs(cans) do
				if not st:is_empty() then
					local pfx = 'starlit_canister_' .. tostring(i)
					local sc = starlit.item.container(st, inv, {pfx = pfx})
					if fn(sc, st, i, pfx) then
						inv:set_stack('starlit_suit_canisters', i, st)
					end
				end
			end end
		end;
		]]
		establishInventories = function(self, obj)
			local inv = obj:get_inventory()
			local ct = suitContainer(self.item, inv)
			ct:pull()
			--[[
			self:pullCanisters(inv)

			local def = self:def()
			local sst = suitStore(self.item)
			local function readList(listName, prop)
				inv:set_size(listName, def.slots[prop])
				if def.slots[prop] > 0 then
					local lst = sst.read(prop)
					inv:set_list(listName, lst)
				end
			end
			readList('starlit_suit_chips', 'chips')
			readList('starlit_suit_bat',   'batteries')
			readList('starlit_suit_guns',  'guns')
			readList('starlit_suit_elem',  'elements')
			readList('starlit_suit_ammo',  'ammo')
			]]
		end;
	};
}

-- TODO find a better place for this!
starlit.type.suit.purgeInventories = function(obj)
	local inv = obj:get_inventory()
	starlit.item.container.dropPrefix(inv, 'starlit_suit')
	starlit.item.container.dropPrefix(inv, 'starlit_canister')
	--[[inv:set_size('starlit_suit_bat', 0)
	inv:set_size('starlit_suit_guns', 0)
	inv:set_size('starlit_suit_chips', 0)
	inv:set_size('starlit_suit_ammo', 0)
	inv:set_size('starlit_suit_elem', 0)
	]]
end

starlit.item.suit.foreach('starlit:suit-gen', {}, function(id, def)
	local icon = lib.image(def.img or 'starlit-item-suit.png')

	local iconColor = def.iconColor
	if not iconColor then
		iconColor = (def.tex and def.tex.plate and def.tex.plate.tint)
			or def.defaultColor
		iconColor = iconColor:to_hsl()
		iconColor.lum = 0
	end

	if iconColor then icon = icon:shift(iconColor) end

	if not def.adorn then
		function def.adorn(a, item, persona)
			local function imageFor(pfx)
				return lib.image(string.format("%s-%s-%s.png", pfx, persona.species, persona.speciesVariant))
			end
			if not def.tex then return end
			a.suit = {}
			for name, t in pairs(def.tex) do
				local img = imageFor(t.id)
				local color

				local cstr = item:get_meta():get_string('starlit:tint_suit_' .. name)
				if cstr and cstr ~= '' then
					color = lib.color.unmarshal(cstr)
				elseif t.tint then
					color = t.tint or def.defaultColor
				end

				if color then
					local hsl = color:to_hsl()
					local adjusted = {
						hue = hsl.hue;
						sat = hsl.sat * 2 - 1;
						lum = hsl.lum * 2 - 1;
					}
					img = img:shift(adjusted)
				end

				a.suit[name] = img
			end
		end
	end

	minetest.register_tool(id, {
		short_description = def.name;
		description = starlit.ui.tooltip {
			title = def.name;
			desc = def.desc;
			color = lib.color(.1, .7, 1);
		};
		groups = {
			suit = 1;
			inv = 1; -- has inventories
			batteryPowered = 1; -- has a battery inv 
			programmable = 1; -- has a chip inv
		};
		on_use = function(st, luser, pointed)
			local user = starlit.activeUsers[luser:get_player_name()]
			if not user then return end
			-- have mercy on users who've lost their suits and wound
			-- up naked and dying of exposure
			if user:naked() then
				local ss = st:take_item(1)
				user:changeSuit(starlit.type.suit(ss))
				user:suitSound('starlit-suit-don')
				return st
			end
		end;
		inventory_image = icon:render();
		_starlit = {
			container = {
				workbench = {
					order = {'batteries','chips','guns','ammo'}
				};
				list = {
					bat = {
						key = 'starlit:suit_slots_bat';
						accept = 'dynamo';
						sz = def.slots.batteries;
					};
					chips = {
						key = 'starlit:suit_slots_chips';
						accept = 'chip';
						sz = def.slots.chips;
					};
					canisters = {
						key = 'starlit:suit_slots_canisters';
						accept = 'canister';
						sz = def.slots.canisters;
					};
					guns = {
						key = 'starlit:suit_slots_gun';
						accept = 'weapon';
						workbench = {
							label = 'Weapon';
							icon = 'starlit-ui-icon-gun';
							color = lib.color(1,0,0);
						};
						sz = def.slots.guns;
					};
					ammo = {
						key = 'starlit:suit_slots_ammo';
						accept = 'ammo';
						workbench = {
							label = 'Ammunition';
							color = lib.color(1,.5,0);
							easySlots = true; -- all slots accessible on the go
						};
						sz = def.slots.ammo;
					};
				};
			};
			event = {
				create = function(st,how)
					local s = suitStore(st)
					-- make sure there's a defined powerstate
					starlit.type.suit(st):powerStateSet 'off'
					suitContainer(st):clear()
					--[[ populate meta tables
					s.write('batteries', {})
					s.write('guns', {})
					s.write('ammo', {})
					s.write('elements', {})
					s.write('chips', {})]]
				end;
			};
			suit = def;
		};
	});
end)

local slotProps = {
	starlit_cfg = {
		itemClass = 'inv';
	};
	starlit_suit_bat = {
		suitSlot = true;
		powerLock = true;
		itemClass = 'dynamo';
	};
	starlit_suit_chips = {
		suitSlot = true;
		powerLock = true;
		itemClass = 'chip';
	};
	starlit_suit_guns = {
		suitSlot = true;
		maintenanceNode = '';
		itemClass = 'suitWeapon';
	};
	starlit_suit_ammo = {
		suitSlot = true;
		maintenanceNode = '';
		itemClass = 'suitAmmo';
	};
	starlit_suit_canisters = {
		suitSlot = true;
		itemClass = 'canister';
	};
}

minetest.register_allow_player_inventory_action(function(luser, act, inv, p)
	local user = starlit.activeUsers[luser:get_player_name()]
	local function grp(i,g)
		return minetest.get_item_group(i:get_name(), g) ~= 0
	end
	local function checkBaseRestrictions(list)
		local restrictions = slotProps[list]
		if not restrictions then return nil, true end
		if restrictions.suitSlot then
			if user:naked() then return restrictions, false end
		end
		if restrictions.powerLock then
			if user:getSuit():powerState() ~= 'off' then return restrictions, false end
		end
		return restrictions, true
	end
	local function itemFits(item, list)
		local rst, ok = checkBaseRestrictions(list)
		if not ok then return false end
		if rst == nil then return true end

		if rst.itemClass and not grp(item, rst.itemClass) then
			return false
		end
		if rst.maintenanceNode then return false end
		-- FIXME figure out best way to identify when the player is using a maintenance node

		if grp(item, 'specialInventory') then
		end

		return true
	end
	local function itemCanLeave(item, list)
		local rst, ok = checkBaseRestrictions(list)
		if not ok then return false end
		if rst == nil then return true end

		if minetest.get_item_group(item:get_name(), 'specialInventory') then

		end

		if rst.maintenanceNode then return false end
		return true
	end

	if act == 'move' then
		local item = inv:get_stack(p.from_list, p.from_index)
		if not (itemFits(item, p.to_list) and itemCanLeave(item, p.from_list)) then
			return 0
		end
	elseif act == 'put' then
		if not itemFits(p.stack, p.listname) then return 0 end
	elseif act == 'take' then
		if not itemCanLeave(p.stack, p.listname) then return 0 end
	end
	return true
end)

minetest.register_on_player_inventory_action(function(luser, act, inv, p)
	local user = starlit.activeUsers[luser:get_player_name()]
	local function slotChange(slot,a,item)
		local s = slotProps[slot]
		if slot == 'starlit_suit' then
			user:updateSuit()
			if user:naked() then
				starlit.type.suit.purgeInventories(user.entity)
				user.power.nano = {}
			end
		elseif s and s.suitSlot then
			local s = user:getSuit()
			s:onItemMove(user, slot, a, item)
			s:onReconfigure(user.entity:get_inventory())
			user:setSuit(s)
		else return end
		user:updateHUD()
	end

	if act == 'put' or act == 'take' then
		local item = p.stack
		slotChange(p.listname, act, item)
	elseif act == 'move' then
		local item = inv:get_stack(p.to_list, p.to_index)
		slotChange(p.from_list, 'take', item)
		slotChange(p.to_list, 'put', item)
	end
end)

local suitInterval = 2.0
starlit.startJob('starlit:suit-software', suitInterval, function(delta)
	local runState = {
		pgmsRun = {};
		flags = {};
	}
	for id, u in pairs(starlit.activeUsers) do
		if not u:naked() then
			local reconfSuit = false
			local inv = u.entity:get_inventory()
			local chips = inv:get_list('starlit_suit_chips')
			local suitprog = starlit.mod.electronics.chip.usableSoftware(chips)
			for _, prop in pairs(suitprog) do
				local s = prop.sw
				if s.kind == 'suitPower' and (s.powerKind == 'passive' or s.bgProc) and (not runState.pgmsRun[s]) then
					local conf = prop.file.body.conf
					local enabled = true
					for _, e in ipairs(conf) do
						if e.key == 'disable'  and e.value == 'yes' then
							enabled = false
							break
						end
					end
					local fn if s.powerKind == 'passive'
						then fn = s.run
						else fn = s.bgProc
					end
					function prop.saveConf(cfg) cfg = cfg or conf
						prop.fd:write(cfg)
						inv:set_stack('starlit_suit_chips', prop.chipSlot, prop.fd.chip)
						reconfSuit = true
					end
					function prop.giveItem(st)
						u:thrustUpon(st)
					end
					
					if enabled and fn(u, prop, suitInterval, runState) then
						runState.pgmsRun[s] = true
					end
				end
			end
			if reconfSuit then
				u:reconfigureSuit()
			end
		end
	end
end)

