-- eventually this will need to be a powered node,
-- but for now i'm just overlooking the need for
-- power and making it work for free
local constants = {
	mill_refresh = 1;
	-- interval at which the mill progress is checked
	-- the formspec is updated

	grind_range = 20;
	-- maximum difference between the hardness of
	-- any two metals in the database
	
	grind_grace_range = 2;
	-- how many levels of hardness a grind head
	-- can grind above its own (while taking massive
	-- levels of damage)

	grind_grace_penalty = 3;
	-- factor by which wear is increased when grinding
	-- above your paygrade

	grind_factor = 10;
	-- adjusts the amount of time it takes to grind
	-- metal into powder
	
	grind_wear = 2000;
	-- amount of damage done to a grind head grinding
	-- metal of equal hardness. increased or decreased
	-- depending on differential
	
	grind_torque_factor = 0.1;
	-- how many points of hardness translate into how
	-- much ley-current needed (per second)
	
	metal_grindvalue = 4;
	-- how many powders an ingot is worth

	default_grindvalue = 1;
	-- number of items produced when not otherwise
	-- specified

	default_grindcost = 1;
	-- number of items needed to perform a grind when
	-- not otherwise specified
	
	default_hardness = 1;
	-- difficulty to grind an item when not otherwise
	-- specified

	metal_grindcost = 1;
	-- number of metal items needed to perform a grind
}
local mill_formspec_update = function(pos,pct,stat1,stat2)
	-- eventually we'll want to display available
	-- energy here

	-- local meta = minetest.get_meta(pos)
	-- local inv = meta:get_inventory()
	-- local torque = 20
	stat1 = stat1 or 'off'
	minetest.get_meta(pos):set_string('formspec', string.format([[
		size[8,7.2]
		list[context;grinder;2.5,1;1,1;0]
		list[context;grinder;4.5,1;1,1;1]
		list[context;input;3.5,0.2;1,1]
		list[context;output;2,2.2;4,1]
		image[3.5,1.2;1,1;gui_furnace_arrow_bg.png^[lowpart:%u%%:gui_furnace_arrow_fg.png^[transformR180]

		image[1.6,1.0;1,1;sorcery_statlamp_%s.png]
		image[5.4,1.0;1,1;sorcery_statlamp_%s.png]
		
		image[2.5,1;1,1;sorcery_mill_grindhead_shade.png]
		image[4.5,1;1,1;sorcery_mill_grindhead_shade.png]

		list[current_player;main;0,3.5;8,4]
	]], 100*pct, stat1, stat2 or stat1))
end
local mill_update = function(pos)
	minetest.get_node_timer(pos):start(constants.mill_refresh)
end
local mill_fits_in_slot = function(slot,item)
	if slot == 'grinder' then
		if minetest.get_item_group(item:get_name(), 'sorcery_mill_grindhead')~=0 
			then return 1 end
	elseif slot == 'input' then
		local metal = sorcery.data.metallookup[item:get_name()]
		local mat = sorcery.matreg.lookup[item:get_name()]
		local comp = sorcery.data.compat.grindables[item:get_name()]
		if metal or (mat and mat.metal) or comp then
			return item:get_count()
		else
			mat = item:get_definition()._matprop
			if mat and mat.grindvalue then
				return item:get_count() 
			end
		end
	end
	return 0
end
local matprops = function(item)
	local metal = sorcery.data.metallookup[item:get_name()]
	if not metal then
		-- allow grinding of armor and tools back to their
		-- original components
		local mat = sorcery.matreg.lookup[item:get_name()]
		if mat and mat.metal then
			metal = mat
		end
	end
	local mp = item:get_definition()._matprop
		or sorcery.data.compat.grindables[item:get_name()]
		or {}

	if metal then mp = {
		hardness = mp.hardness or metal.data.hardness;
		grindvalue = ((mp.grindvalue or metal.value) or (metal and constants.metal_grindvalue));
		powder = mp.powder or metal.data.parts.powder;
		grindcost = mp.grindcost or constants.metal_grindcost; -- invariant for metal
	} end

	mp.torque     = constants.grind_torque_factor * mp.hardness
	mp.grindvalue = mp.grindvalue or constants.default_grindvalue
	mp.grindcost  = mp.grindcost  or constants.default_grindcost
	mp.hardness   = mp.hardness   or constants.default_hardness;

	if item:get_wear() ~= 0 then
		-- prevent cheating by recovering metal from items before they
		-- are destroyed
		local wearfac = (item:get_wear() / 65535)
		mp.grindvalue = math.max(1,math.ceil(mp.grindvalue * wearfac))
		mp.hardness = math.max(1,math.ceil(mp.grindcost * wearfac))
		mp.torque = math.max(1,math.ceil(mp.torque * wearfac))
	end

	return mp
end
minetest.register_node('sorcery:mill',{
	description = 'Mill';
	groups = {
		cracky = 2;
		sorcery_ley_device = 1;
		sorcery_metallurgy = 1;
	};
	paramtype2 = 'facedir';
	after_dig_node = sorcery.lib.node.purge_container;
	on_construct = function(pos)
		local meta = minetest.get_meta(pos)
		local inv = meta:get_inventory()
		inv:set_size('input',1)
		inv:set_size('output',4)
		inv:set_size('grinder',2)
		meta:set_float('grindtime',0)
		meta:set_int('active',0)
		mill_formspec_update(pos, 0)
	end;
	on_metadata_inventory_put = function(pos,listname)
		if listname == 'input' then
			minetest.get_meta(pos):set_float('grindtime',0)
		end
		mill_update(pos)
	end;
	on_metadata_inventory_take = function(pos,listname)
		if listname == 'input' then
			minetest.get_meta(pos):set_float('grindtime',0)
		end
		mill_update(pos)
	end;
	on_metadata_inventory_move = function(pos,fl,fi,tl,ti)
		if fl == 'input' or tl == 'input' then
			minetest.get_meta(pos):set_float('grindtime',0)
		end
		mill_update(pos)
	end;
	_sorcery = {
		ley = {
			mode = 'consume', affinity = {'praxic'};
			power = function(pos,time)
				local meta = minetest.get_meta(pos)
				local active = meta:get_int('active') == 1
				if not active then return 0 end
				local inv = meta:get_inventory()
				local item = inv:get_stack('input',1)
				if item:is_empty() then
					meta:set_int('active', 0)
					return 0
				end
				return matprops(item).torque * time;
			end;
		};
		on_leychange = mill_update;
	};
	on_timer = function(pos,delta)
		local meta = minetest.get_meta(pos)
		local inv = meta:get_inventory()
		local elapsed = meta:get_float('grindtime') + delta
		local ley = sorcery.ley.netcaps(pos,delta,pos)
		local again = false
		local active = false
		local reqtime -- sigh
		local statcolor = 'off'
		local grinders_on
		if inv:is_empty('input') or inv:is_empty('grinder') then
			elapsed = 0
			mill_formspec_update(pos, 0)
		else
			local item = inv:get_stack('input',1)
			local mp = matprops(item)
			if mp.grindcost > item:get_count() then
				elapsed = 0
				mill_formspec_update(pos, 0)
				goto stop
			end

			if ley.maxpower < (mp.torque * delta) then
				-- not enough potential energy in the system to grind
				-- so don't bother
				statcolor = 'red'
				elapsed = 0 goto stop
			elseif ley.freepower < (mp.torque * delta) then
				-- the net has enough potential energy to supply us,
				-- but too much of it is in use right now. give up
				-- on this round, but try again in a bit to see if
				-- more energy is available
				statcolor = 'yellow'
				elapsed = 0 again = true goto stop
			end

			local grinders = 0
			local grindpower = 0
			local grind_wear = {}
			local grinders_present = false
			grinders_on = {false,false}
			for i=1,inv:get_size('grinder') do
				local gh = inv:get_stack('grinder',i)
				if not gh:is_empty() then
					grinders_present = true
					local hh = sorcery.data.metals[gh:get_definition()._proto.metal].hardness
					local dif = mp.hardness - hh
					local wear
					if dif == 0 then
						wear = constants.grind_wear
					elseif dif < 0 then
						wear = constants.grind_wear * ((dif * -1)/constants.grind_range)
					elseif dif > 0 then
						if dif > constants.grind_grace_range then
							wear = 0
							goto reject
						else
							wear = constants.grind_wear * (1 + (dif/constants.grind_range)) * constants.grind_grace_penalty
						end
					end
					::accept:: grinders = grinders + 1
					           grindpower = grindpower + hh
							   grinders_on[i] = true
					::reject:: grind_wear[i] = wear
				end
			end
			if grinders == 0 then
				if grinders_present then
					statcolor = 'purple'
				end
				elapsed = 0
				mill_formspec_update(pos, 0)
			else
				active = true again = true 
				if grindpower < mp.hardness then
					statcolor = 'yellow'
				else statcolor='green' end
				grindpower = grindpower / grinders
				-- if there is more power available than needed,
				-- and/or if the blades are stronger than needed,
				-- speed up the grind
				local speedboost = math.max(0.05,((grindpower - mp.hardness)/constants.grind_range) * grinders)
				reqtime = mp.grindvalue * mp.hardness * constants.grind_factor * (1-speedboost)
				if elapsed >= reqtime then
					item:take_item(mp.grindcost)
					inv:set_stack('input',1,item)
					local pw = ItemStack{
						name=mp.powder;
						count=mp.grindvalue;
					}
					if inv:room_for_item('output',pw) then
						inv:add_item('output',pw)
					else
						minetest.add_item(pos,pw)
					end
					elapsed = 0
					for i,d in pairs(grind_wear) do
						local item = inv:get_stack('grinder',i)
						item:add_wear(grind_wear[i])
						inv:set_stack('grinder',i,item)
					end
				end
			end
		end
		
		::stop:: meta:set_float('grindtime',elapsed)
		         local sc1, sc2 = 'off','off'
				 if grinders_on then
					 sc1 = grinders_on[1] and statcolor or sc1
					 sc2 = grinders_on[2] and statcolor or sc2
				 else
					 sc1 = statcolor sc2=sc1
				 end
		         mill_formspec_update(pos, active and (elapsed/reqtime) or 0, sc1,sc2)
		         meta:set_int('active',active and 1 or 0)
		         return again
	end;
	allow_metadata_inventory_put = function(pos,list,idx,stack,user)
		return mill_fits_in_slot(list,stack) end;
	allow_metadata_inventory_move = function(pos,fromlist,fromidx,tolist,toidx,count,user)
		local meta = minetest.get_meta(pos)
		local inv = meta:get_inventory()
		local stack = inv:get_stack(fromlist,fromidx)
		return mill_fits_in_slot(tolist,stack)
	end;
	tiles = {
		'sorcery_mill_top.png^[transformR90';
		'sorcery_mill_bottom.png';
		'sorcery_mill_side.png';
		'sorcery_mill_side.png';
		'sorcery_mill_back.png';
		'sorcery_mill_front.png';
	};
})
minetest.register_craft {
	output = 'sorcery:mill';
	recipe = {
		{'basic_materials:chain_steel','basic_materials:gear_steel','basic_materials:chain_steel'};
		{'basic_materials:gear_steel', 'basic_materials:steel_bar', 'basic_materials:gear_steel'};
		{'default:tin_ingot','default:steelblock','default:tin_ingot'};
	};
}
for name,metal in pairs(sorcery.data.metals) do
	local i,f = metal.parts.ingot, metal.parts.fragment
	local id = 'sorcery:mill_grindhead_' .. name
	minetest.register_tool(id,{
		description = sorcery.lib.str.capitalize(name) .. ' Grinding Head';
		inventory_image = sorcery.lib.image('sorcery_mill_grindhead.png'):multiply(sorcery.lib.color(metal.tone)):render();
		groups = { sorcery_mill_grindhead = 1, sorcery_metallurgy = 1 };
		_proto = {
			metal = name;
		};
	});
	minetest.register_craft {
		output = id;
		recipe = {
			{f,i,f};
			{i,'',i};
			{f,i,f};
		};
	}
end
