-- [ʞ] fab.lua
--  ~ lexi hale <lexi@hale.su>
--  🄯 EUPL1.2
--  ? fabrication spec class
--    a type.fab supports two operators:
--
--    + used for compounding recipes. that is,
--			a+b = compose a new spec from the spec parts a and b.
--      this is used e.g. for creating tier-based
--      fabspecs.
--
--    * used for determining quantities. that is,
--			f*x = spec to make x instances of f
--
--    new fab fields must be defined in starlit.type.fab.fields.
--    this maps a name to fn(a,b,n) -> quant, where a is the first
--    argument, b is a compounding amount, and n is a quantity of
--    items to produce. fields that are unnamed will be underwritten

local fab

local function fQuant(a,b,n) return ((a or 0)+(b or 0))*n end
local function fFac  (a,b,n)
	if a == nil and b == nil then return nil end
	local f if a == nil or b == nil then
		f = a or b
	else
		f = (a or 1)*(b or 1)
	end
	return f*n
end
local function fReq  (a,b,n) return a or b         end
local function fFlag (a,b,n) return a and b        end
local function fSize (a,b,n) return math.max(a,b)  end

local F = string.format
local lib = starlit.mod.lib

local function fRawMat(class)
	return function(x,n,stack)
		local def = stack:get_definition()._starlit
		if not def.material then return 0 end
		local mf = fab {[def.material.kind] = {[def.material[def.material.kind]] = def.mass}}

--		this is bugged: the same item can satisfy both e.g. metal.steel and element.fe
-- 		if not (mf[class] and mf[class][x]) then
-- 			mf = mf:elementalize()
			if not (mf[class] and mf[class][x]) then return 0 end
-- 		end

		local perItem = mf[class][x]
		local wholeStack = perItem * stack:get_count()

		local deduct = ItemStack()
		local taken = 0 repeat
			taken = taken + perItem
			deduct:add_item(stack:take_item(1))
		until taken >= n or stack:is_empty()
		return taken, deduct

		--[[  outsmarted myself with this one :/
		local fab = def.recover or def.fab
		-- we ignore recover_vary bc this needs to be deterministic
		local function tryFab(fab)
			if not fab then return 0 end
			if fab[class] and fab[class][x] then
				local perItem = fab[class][x]
				local wholeStack = perItem * stack:get_count()
				print('fab has substance', n, perItem, wholeStack)
				local deduct = ItemStack()
				local taken = 0 repeat
					taken = taken + perItem
					deduct:add_item(stack:take_item(1))
				until taken >= n
				return taken, deduct
			end
			return 0
		end
		local z,c = tryFab(fab)
		if z == 0 then -- does it work if we break down the constituent compounds?
			z,c = tryFab(fab:elementalize())
		end]]
	end
end
local function fCanister(class)
	return function(x, n, stack)
		local amt, deduct = 0
		return amt, deduct
	end
end

local fields = {
	-- fabrication eligibility will be determined by which kinds
	-- of input a particular fabricator can introduce. e.g. a
	-- printer with a  but no cache can only print items whose
	-- recipe only names elements as ingredients
	element = {
		name = {"element", "elements"};
		string = function(x, n, long)
			local el = starlit.world.material.element.db[x]
			return lib.math.siUI('g', n) .. ' ' .. ((not long and el.sym) or el.name)
		end;
		image = function(x, n)
			return string.format('starlit-element-%s.png', x)
		end;
		inventory = fRawMat 'element';
		op = fQuant;
	};
	metal = {
		name = {"metal", "metals"};
		string = function(x, n)
			local met = starlit.world.material.metal.db[x]
			return lib.math.siUI('g', n) .. ' ' .. met.name
		end;
		image = function(x, n)
			local met = starlit.world.material.metal.db[x]
			return ItemStack(met.form.brick):get_definition().inventory_image
		end;
		inventory = fRawMat 'metal';
		op = fQuant;
	};
	liquid = {
		name = {"liquid", "liquids"};
		string = function(x, n)
			local liq = starlit.world.material.liquid.db[x]
			return lib.math.siUI('L', n) .. ' ' .. liq.name
		end;
		inventory = fCanister 'liquid';
		op = fQuant;
	};
	gas = {
		name = {"gas", "gasses"};
		string = function(x, n)
			local gas = starlit.world.material.gas.db[x]
			return lib.math.siUI('g', n) .. ' ' .. gas.name
		end;
		inventory = fCanister 'gas';
		op = fQuant;
	};
-- 	crystal = {
-- 		op = fQuant;
-- 	};
	item = {
		name = {"item", "items"};
		string = function(x, n)
			local i = minetest.registered_items[x]
			return tostring(n) .. 'x ' .. i.short_description
		end;
		image = function(x, n)
			return ItemStack(x):get_definition().inventory_image
		end;
		inventory = function(x, n, stack)
			x = ItemStack(x)
			if not x:equals(stack) then return nil end
			local deduct = stack:take_item(x:get_count() * n)
			return deduct:get_count(), deduct
		end;
	};

	-- factors

	cost = {
		name = {"cost", "costs"};
		op=fFac; -- units vary
		string = function(x,n)
			local units = {
				power = 'J';
			}
			local s
			if units[x] then
				s = lib.math.siUI(units[x], n)
			elseif starlit.world.stats[x] then
				s = starlit.world.stats[x].desc(n)
			else
				s = tostring(n)
			end
			return string.format('%s: %s',x,s)
		end;
		image = function(x,n)
			local icons = {
				power = 'starlit-ui-icon-stat-power.png';
				numina = 'starlit-ui-icon-stat-numina.png'
			}
			return icons[x]
		end;
	};
	time = {op=fFac}; -- (s)
		-- print: base printing time
	size = {op=fSize};
		-- printBay: size of the printer bay necessary to produce the item
	req  = {op=fReq};
	flag = {op=fFlag}; -- means that can be used to produce the item & misc flags
		-- print: allow production with a printer
		-- smelt: allow production with a smelter
	-- all else defaults to underwrite
}

local order = {
	'element', 'metal', 'liquid', 'gas', 'item',
	'cost'
}

local lib = starlit.mod.lib

fab = lib.class {
	__name = 'starlit:fab';
	
	fields = fields;
	order = order;
	construct = function(q) return q end;
	__index = {
		elementalize = function(self)
			local e = fab {element = self.element or {}}
			for _, kind in pairs {'metal', 'gas', 'liquid'} do
				for m,mass in pairs(self[kind] or {}) do
					local mc = starlit.world.material[kind].db[m].composition
					e = e + mc:elementalize()*mass
				end
			end
			return e
		end;

		elementSeq = function(self)
			local el = {}
			local em = self.element
			local s = 0
			local eldb = starlit.world.material.element.db
			for k in pairs(em) do table.insert(el, k) s=s+eldb[k].n end
			table.sort(el, function(a,b)
				return eldb[a].n > eldb[b].n
			end)
			return el, em, s
		end;

		formula = function(self)
			local ts,f=0
			if self.element then
				f = {}
				local el, em, s = self:elementSeq()
				local eldb = starlit.world.material.element.db
				for i, e in ipairs(el) do
					local sym, n = eldb[e].sym, em[e]
					if n > 0 then
						table.insert(f, string.format("%s%s",
							sym, n>1 and lib.str.nIdx(n) or ''))
					end
				end
				f = table.concat(f)
				ts = ts + s
			end

			local sub = {}
			for _, w in pairs {'metal', 'gas', 'liquid'} do
				if self[w] then
					local mdb = starlit.world.material[w].db
					for k, amt in pairs(self[w]) do
						local mf, s = mdb[k].composition:formula()
						if amt > 0 then table.insert(sub, {
							f = string.format("(%s)%s",mf,
								lib.str.nIdx(amt));
							s = s;
						}) end
						ts = ts + s*amt
					end
				end
			end
			table.sort(sub, function(a,b) return a.s > b.s end)
			local fml = {}
			for i, v in ipairs(sub) do fml[i] = v.f end
			if f then table.insert(fml, f) end
			fml = table.concat(fml, ' + ')

			return fml, ts
		end;

		visualize = function(self)
			local all = {}
			for i,o in ipairs(order) do
				local t = {}
				if self[o] then
					for mat,amt in pairs(self[o]) do
						local v = {}
						v.id = mat
						v.n = amt
						if fields[o].string then
							v.label = fields[o].string(mat,amt,true)
						end
						if fields[o].image then
							v.img = fields[o].image(mat,amt)
						end
						table.insert(t,v)
					end
				end
				if fields[o].sort then
					table.sort(t, function(a,b) return fields[o].sort(a.id, b.id) end)
				end
				if next(t) then table.insert(all, {
					id=o, list=t;
					header=fields[o].name[t[2] and 2 or 1];
				}) end
			end
			return all
		end;
		seek = function(self, invs)
			local consumed = {}
			local spec = fab{item={}} -- used to generate a convenient visualization
			local unsatisfied = fab{}
			local cache = {}
			local leftover = fab{}
			local function alreadyGot(inv,slot)
				local already = cache[inv] and cache[inv][slot] and true
				if cache[inv] == nil then cache[inv] = {} end
				cache[inv][slot] = true
				return already
			end
			for ci, cat in ipairs(order) do
				local scan = fields[cat].inventory
				if scan and self[cat] then
					for substance, amt in pairs(self[cat]) do
-- 						print('check substance', substance, amt, dump(self[cat]))
						local amtFound = 0
						local stacks = {}
						for ii, inv in ipairs(invs) do
-- 							print('  - check inventory',ii,inv,'for',cat,substance,amt)
							for oi, o in ipairs(inv) do
-- 								print('    - check stack', oi, o)
								local st = ItemStack(o)
								if not st:is_empty() then
									local avail, deduct = scan(substance,amt,st)
									if avail > 0 then
										amtFound = amtFound + avail
-- 										print('       - found amt', amtFound,ii,oi)
										if not alreadyGot(ii,oi) then
											local sv = {
												inv=ii, slot=oi;
												consume=deduct, remain=st;
												satisfy=fab{[cat]={[substance]=avail}}
											}
											table.insert(stacks, sv)
											table.insert(consumed, sv)
										end
										if amtFound >= amt then goto suffice end
									end
								end
							end
						end

						::insufficient:: do -- record the failure and move on
							if unsatisfied[cat] == nil then unsatisfied[cat] = {} end
							unsatisfied[cat][substance] = amt-amtFound
						end

						::suffice:: -- commit the stack diff
						for si,sv in ipairs(stacks) do
-- 							table.insert(consumed, sv)
							local di = ItemStack(sv.consume)
							local din = ItemStack(sv.consume):get_name()
							if not spec.item[din] then spec.item[din] = 0 end
							spec.item[din] = spec.item[din] + di:get_count()
							local lo = amtFound-amt if lo > 0 then
								leftover = leftover + fab{[cat]={[substance]=lo}}
							end
						end

					end
				end
			end
			return (next(unsatisfied) == nil), consumed, unsatisfied, leftover, spec
		end;
	};

	__tostring = function(self)
		local t = {}
		for i,o in ipairs(order) do
			if self[o] and fields[o].string then
				for mat,amt in pairs(self[o]) do
					if amt > 0 then
						table.insert(t, fields[o].string(mat, amt))
					end
				end
			end
		end
		return table.concat(t, ", ")
	end;

	__add = function(a,b)
		local new = fab {}
		for cat, vals in pairs(a) do
			new[cat] = lib.tbl.copy(vals)
		end
		for cat, vals in pairs(b) do
			if not new[cat] then
				new[cat] = lib.tbl.copy(vals)
			else
				local f = fields[cat].op
				for k,v in pairs(vals) do
					local n = f(new[cat][k], v, 1)
					if type(n) == 'number' and n < 0 then n = nil end
					new[cat][k] = n
				end
			end
		end
		return new
	end;

	__mul = function(x,n)
		if n == 1 then return fab.clone(x) end
		local new = fab {}
		if n == 0 then return new end
		for cat, vals in pairs(x) do
			new[cat] = {}
			local f = fields[cat].op
			for k,v in pairs(vals) do
				local num = f(v,nil,n)
				if type(num) == 'number' and n < 0 then n = nil end
				new[cat][k] = num
			end
		end
		return new
	end;

	__div = function(x,n)
		return x * (1/n)
	end;
}

starlit.type.fab = fab
