-- [ʞ] 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 starsoul.type.fab.opClass.
--    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 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 opClass = {
	-- 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

	-- ingredients
	element    = fQuant; -- (g)
	gas        = fQuant; -- ()
	liquid     = fQuant; -- (l)
	crystal    = fQuant; -- (g)
	item       = fQuant; -- n
	metal      = fQuant; -- (g)
	metalIngot = fQuant; -- (g)
	-- factors
	cost = fFac; -- units vary
	time = fFac; -- (s)
		-- print: base printing time
	size = fSize;
		-- printBay: size of the printer bay necessary to produce the item
	req  = fReq;
	flag = 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 F = string.format
local strClass = {
	element = function(x, n)
		local el = starsoul.world.material.element[x]
		return lib.math.si('g', n) .. ' ' .. (el.sym or el.name)
	end;
	metal = function(x, n)
		local met = starsoul.world.material.metal[x]
		return lib.math.si('g', n) .. ' ' .. met.name
	end;
	liquid = function(x, n)
		local liq = starsoul.world.material.liquid[x]
		return lib.math.si('L', n) .. ' ' .. liq.name
	end;
	gas = function(x, n)
		local gas = starsoul.world.material.gas[x]
		return lib.math.si('g', n) .. ' ' .. gas.name
	end;
	item = function(x, n)
		local i = minetest.registered_items[x]
		return tostring(n) .. 'x ' .. i.short_description
	end;
}

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

local lib = starsoul.mod.lib
local fab fab = lib.class {
	__name = 'starsoul:fab';
	
	opClass = opClass;
	strClass = strClass;
	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 = starsoul.world.material[kind][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 = starsoul.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)
			print('make formula', dump(self))
			local ts,f=0
			if self.element then
				f = {}
				local el, em, s = self:elementSeq()
				local eldb = starsoul.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 = starsoul.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;
	};

	__tostring = function(self)
		local t = {}
		for i,o in ipairs(order) do
			if self[o] then
				for mat,amt in pairs(self[o]) do
					if amt > 0 then
						table.insert(t, strClass[o](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 = opClass[cat]
				for k,v in pairs(vals) do
					local n = f(new[cat][k], v, 1)
					new[cat][k] = n > 0 and n or nil
				end
			end
		end
		return new
	end;

	__mul = function(x,n)
		local new = fab {}
		for cat, vals in pairs(x) do
			new[cat] = {}
			local f = opClass[cat]
			for k,v in pairs(vals) do
				local num = f(v,nil,n)
				new[cat][k] = num > 0 and num or nil
			end
		end
		return new
	end;

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

starsoul.type.fab = fab
