--[[

	Tubelib Addons 1
	================

	Copyright (C) 2017-2021 Joachim Stolberg

	AGPL v3
	See LICENSE.txt for more information

	harvester.lua

	Harvester machine to chop wood, leaves and harvest farming crops and flowers.

	The machine is able to harvest an square area of up to 33x33 blocks (radius = 16).
	The base node has to be placed in the middle of the harvesting area.
	The Harvester processes one node every 6 seconds.
	It requires one item Bio Fuel per 20 nodes.

]]--

-- Load support for I18n
local S = tubelib_addons1.S

-- for lazy programmers
local P = minetest.string_to_pos
local M = minetest.get_meta

local CYCLE_TIME = 6
local START_HEIGHT = 18  -- harvesting altitude
local MAX_DIAMETER = 33
local BURNING_TIME = 20  -- fuel
local STANDBY_TICKS = 4  -- used for blocked state
local COUNTDOWN_TICKS = 2
local OFFSET = 5  -- for uneven terrains

-- start on top of the base block
local function working_start_pos(pos, altitude)
	local working_pos = table.copy(pos)
	working_pos.y = working_pos.y + (altitude or START_HEIGHT)
	return working_pos
end

local Radius2Idx = {[4]=1 ,[6]=2, [8]=3, [10]=4, [12]=5, [14]=6, [16]=7}
local Altitude2Idx = {[-2]=1 ,[-1]=2, [0]=3, [1]=4, [2]=5, [4]=6, [6]=7, [8]=8, [10]=9, [14]=10, [18]=11}

local function formspec(self, pos, meta)
	-- some recalculations
	local this = minetest.deserialize(meta:get_string("this"))
	local endless = this.endless == 1 and "true" or "false"
	local fuel = this.fuel * 100/BURNING_TIME
	if self:get_state(meta) ~= tubelib.RUNNING then
		fuel = 0
	end
	local radius = Radius2Idx[this.radius] or 2
	local altitude = Altitude2Idx[this.altitude or START_HEIGHT] or 11

	return "size[9,8]"..
	default.gui_bg..
	default.gui_bg_img..
	default.gui_slots..
	"dropdown[0,0;1.5;radius;4,6,8,10,12,14,16;"..radius.."]"..
	"label[1.6,0.2;"..S("Area radius").."]"..
	"dropdown[0,1;1.5;altitude;-2,-1,0,1,2,4,6,8,10,14,18;"..altitude.."]"..
	"label[1.6,1.2;"..S("Altitude ").."]"..
	"checkbox[0,2;endless;"..S("Run endless")..";"..endless.."]"..
	"list[context;main;5,0;4,4;]"..
	"list[context;fuel;1.5,3;1,1;]"..
	"item_image[1.5,3;1,1;tubelib_addons1:biofuel]"..
	"image[2.5,3;1,1;default_furnace_fire_bg.png^[lowpart:"..
	fuel..":default_furnace_fire_fg.png]"..
	"image_button[3.5,3;1,1;".. self:get_state_button_image(meta) ..";state_button;]"..
	"list[current_player;main;0.5,4.3;8,4;]"..
	"listring[context;main]"..
	"listring[current_player;main]"
end

local State = tubelib.NodeStates:new({
	node_name_passive = "tubelib_addons1:harvester_base",
	node_name_defect = "tubelib_addons1:harvester_defect",
	infotext_name = S("Tubelib Harvester"),
	cycle_time = CYCLE_TIME,
	standby_ticks = STANDBY_TICKS,
	has_item_meter = true,
	aging_factor = 15,
	on_start = function(pos, meta, oldstate)
		local this = minetest.deserialize(meta:get_string("this"))
		this.idx = 0
		this.working_pos = working_start_pos(pos, this.altitude)
		meta:set_string("this", minetest.serialize(this))
	end,
	formspec_func = formspec,
})

local function gen_working_steps()
	-- Working steps like a snail shell from inner to outer
	local t = {}
	for steps = 1,MAX_DIAMETER,2 do
		for idx = 1,steps do
			t[#t+1] = 0
		end
		for idx = 1,steps do
			t[#t+1] = 1
		end
		steps = steps + 1
		for idx = 1,steps do
			t[#t+1] = 2
		end
		for idx = 1,steps do
			t[#t+1] = 3
		end
	end
	return t
end

local WorkingSteps = gen_working_steps()


local function allow_metadata_inventory_put(pos, listname, index, stack, player)
	if minetest.is_protected(pos, player:get_player_name()) then
		return 0
	end
	local inv = M(pos):get_inventory()
	if listname == "main" then
		return stack:get_count()
	elseif listname == "fuel" and tubelib.is_fuel(stack) then
		return stack:get_count()
	end
	return 0
end

local function allow_metadata_inventory_take(pos, listname, index, stack, player)
	if minetest.is_protected(pos, player:get_player_name()) then
		return 0
	end
	return stack:get_count()
end

local function get_next_pos(old_pos, idx)
	local facedir = WorkingSteps[idx]
	return vector.add(old_pos, core.facedir_to_dir(facedir))
end

-- Remove saplings lying arround
local function remove_all_sapling_items(pos)
	for _, object in pairs(minetest.get_objects_inside_radius(pos, 4)) do
		local lua_entity = object:get_luaentity()
		if not object:is_player() and lua_entity and lua_entity.name == "__builtin:item" then
			object:remove()
		end
	end
end

local function is_plantable_ground(node)
	if minetest.get_item_group(node.name, "soil") ~= 0 then
		return true
	end
	if minetest.get_item_group(node.name, "sand") ~= 0 then
		return true
	end
	return false
end

-- Remove wood/leave nodes and place sapling if necessary
-- Return false if inventory is full
-- else return true
local function remove_or_replace_node(this, pos, inv, node, order)
	local next_pos = table.copy(pos)
	next_pos.y = next_pos.y - 1

	-- Not enough space in the inventory
	if not inv:room_for_item("main", ItemStack(node.name)) then
		return false
	end
	local next_node = minetest.get_node_or_nil(next_pos)
	if next_node then
		minetest.remove_node(pos)
		inv:add_item("main", ItemStack(order.drop))
		this.num_items = this.num_items + 1
		if is_plantable_ground(next_node) and order.plant then  -- hit the ground?
			minetest.set_node(pos, {name=order.plant, paramtype2 = "wallmounted", param2=1})
			if order.t1 ~= nil then
				-- We have to simulate "on_place" and start the timer by hand
				-- because the after_place_node function checks player rights and can't therefore
				-- be used.
				minetest.get_node_timer(pos):start(math.random(order.t1, order.t2))
			end
			remove_all_sapling_items(pos)
		end
	end
	return true
end

-- check the fuel level and return false if empty
local function check_fuel(pos, this, meta)
	if this.fuel <= 0 then
		local fuel_item = tubelib.get_this_item(meta, "fuel", 1)
		if fuel_item == nil then
			return false
		end
		if not tubelib.is_fuel(fuel_item) then
			tubelib.put_item(meta, "fuel", fuel_item)
			return false
		end
		this.fuel = BURNING_TIME
	else
		this.fuel = this.fuel - 1
	end
	return true
end

local function calc_new_pos(pos, this, meta)
	this.idx = this.idx + 1
	if this.idx >= this.max then
		if this.endless == 1 then
			this.idx = 0
			this.working_pos = working_start_pos(pos, this.altitude)
			return true
		else
			return false
		end
	end
	this.working_pos = get_next_pos(this.working_pos, this.idx)
	return true
end

-- Scan the space below the given position
-- Return false if inventory is full
-- else return true
local function harvest_field(this, meta)
	local inv = meta:get_inventory()
	local pos = table.copy(this.working_pos)
	local start_y_pos = pos.y - 1
	local stop_y_pos = pos.y - (this.altitude or START_HEIGHT) - OFFSET
	if minetest.is_protected(pos, this.owner) then
		return true
	end
	for y_pos = start_y_pos,stop_y_pos,-1 do
		pos.y = y_pos
		local node = minetest.get_node_or_nil(pos)
		if node and node.name ~= "air" then
			local order = tubelib_addons1.FarmingNodes[node.name] or tubelib_addons1.Flowers[node.name]
			if order then
				if not minetest.is_protected(pos, this.owner) and not remove_or_replace_node(this, pos, inv, node, order) then
					return false
				end
			else
				return true	-- hit the ground
			end
		end
	end
	return true
end

local function not_blocked(pos, this, meta)
	if State:get_state(meta) == tubelib.BLOCKED then
		if harvest_field(this, meta) then
			minetest.after(0, State.start, State, pos, meta)
		end
		return false
	end
	return true
end

-- move the "harvesting copter" to the next pos and harvest the field below
local function keep_running(pos, elapsed)
	if tubelib.data_not_corrupted(pos) then
		local meta = M(pos)
		local this = minetest.deserialize(meta:get_string("this"))
		this.num_items = 0

		if not_blocked(pos, this, meta) then
			if check_fuel(pos, this, meta) then
				if calc_new_pos(pos, this, meta) then
					if harvest_field(this, meta) then
						meta:set_string("this", minetest.serialize(this))
						meta:set_string("infotext",
							S("Tubelib Harvester").." "..this.number..
							S(": running (")..this.idx.."/"..this.max..")")
						State:keep_running(pos, meta, COUNTDOWN_TICKS, this.num_items)
					else
						State:blocked(pos, meta)
					end
				else
					State:stop(pos, meta)
				end
			else
				State:fault(pos, meta)
			end
		end
		return State:is_active(meta)
	end
	return false
end


local function on_receive_fields(pos, formname, fields, player)
	if minetest.is_protected(pos, player:get_player_name()) then
		return
	end
	local meta = M(pos)
	local this = minetest.deserialize(meta:get_string("this"))
	local radius = this.radius
	local altitude = this.altitude or START_HEIGHT

	if fields.radius ~= nil then
		radius = tonumber(fields.radius)
	end
	if radius ~= this.radius then
		this.radius = radius
		this.max = (radius*2 + 1) * (radius*2 + 1)
		meta:set_string("this", minetest.serialize(this))
		State:stop(pos, meta)
	end

	if fields.altitude ~= nil then
		altitude = tonumber(fields.altitude)
	end
	if altitude ~= this.altitude then
		this.altitude = altitude
		meta:set_string("this", minetest.serialize(this))
		State:stop(pos, meta)
	end

	if fields.endless ~= nil then
		this.endless = fields.endless == "true" and 1 or 0
	end
	meta:set_string("this", minetest.serialize(this))

	State:state_button_event(pos, fields)
end

minetest.register_node("tubelib_addons1:harvester_base", {
	description = S("Tubelib Harvester Base"),
	tiles = {
		-- up, down, right, left, back, front
		'tubelib_front.png',
		'tubelib_front.png',
		'tubelib_addons1_harvester.png',
	},

	after_place_node = function(pos, placer)
		local meta = M(pos)
		local inv = meta:get_inventory()
		inv:set_size('main', 16)
		inv:set_size('fuel', 1)
		local number = tubelib.add_node(pos, "tubelib_addons1:harvester_base")
		local this = {
			number = number,
			owner = placer:get_player_name(),
			working_pos = working_start_pos(pos, START_HEIGHT),
			fuel = 0,
			endless = 0,
			radius = 6,
			idx = 0,
			max = (6+1+6) * (6+1+6)
		}
		meta:set_string("this", minetest.serialize(this))
		State:node_init(pos, number)
	end,

	can_dig = function(pos, player)
		if minetest.is_protected(pos, player:get_player_name()) then
			return false
		end
		local inv = M(pos):get_inventory()
		return inv:is_empty("main") and inv:is_empty("fuel")
	end,

	on_dig = function(pos, node, player)
		State:on_dig_node(pos, node, player)
		tubelib.remove_node(pos)
	end,

	on_rotate = screwdriver.disallow,
	on_receive_fields = on_receive_fields,
	on_timer = keep_running,
	allow_metadata_inventory_put = allow_metadata_inventory_put,
	allow_metadata_inventory_take = allow_metadata_inventory_take,

	paramtype = "light",
	sunlight_propagates = true,
	paramtype2 = "facedir",
	groups = {choppy=2, cracky=2, crumbly=2},
	is_ground_content = false,
	sounds = default.node_sound_wood_defaults(),
	on_blast = function() end,
})

minetest.register_node("tubelib_addons1:harvester_defect", {
	description = S("Tubelib Harvester Base"),
	tiles = {
		-- up, down, right, left, back, front
		'tubelib_front.png',
		'tubelib_front.png',
		'tubelib_addons1_harvester.png^tubelib_defect.png',
	},

	on_construct = function(pos)
		local inv = M(pos):get_inventory()
		inv:set_size('main', 16)
		inv:set_size('fuel', 1)
	end,

	after_place_node = function(pos, placer)
		local number = tubelib.add_node(pos, "tubelib_addons1:harvester_base")
		local this = {
			number = number,
			owner = placer:get_player_name(),
			working_pos = working_start_pos(pos, START_HEIGHT),
			fuel = 0,
			endless = 0,
			radius = 6,
			idx = 0,
			max = (6+1+6) * (6+1+6)
		}
		local meta = M(pos)
		meta:set_string("this", minetest.serialize(this))
		State:node_init(pos, number)
		State:defect(pos, meta)
	end,

	can_dig = function(pos, player)
		if minetest.is_protected(pos, player:get_player_name()) then
			return false
		end
		local inv = M(pos):get_inventory()
		return inv:is_empty("main") and inv:is_empty("fuel")
	end,

	after_dig_node = function(pos, oldnode, oldmetadata, digger)
		tubelib.remove_node(pos)
	end,

	on_rotate = screwdriver.disallow,
	allow_metadata_inventory_put = allow_metadata_inventory_put,
	allow_metadata_inventory_take = allow_metadata_inventory_take,

	paramtype = "light",
	sunlight_propagates = true,
	paramtype2 = "facedir",
	groups = {choppy=2, cracky=2, crumbly=2, not_in_creative_inventory=1},
	is_ground_content = false,
	sounds = default.node_sound_wood_defaults(),
	on_blast = function() end,
})


minetest.register_craft({
	output = "tubelib_addons1:harvester_base",
	recipe = {
		{"default:steel_ingot", "default:mese_crystal", "default:steel_ingot"},
		{"default:steel_ingot", "default:mese_crystal",	"tubelib:tubeS"},
		{"group:wood", 			"default:mese_crystal", "group:wood"},
	},
})


tubelib.register_node("tubelib_addons1:harvester_base", {"tubelib_addons1:harvester_defect"}, {
	on_pull_stack = function(pos, side)
		return tubelib.get_stack(M(pos), "main")
	end,
	on_pull_item = function(pos, side)
		return tubelib.get_item(M(pos), "main")
	end,
	on_push_item = function(pos, side, item)
		if not tubelib.is_fuel(item) then
			return false
		end
		return tubelib.put_item(M(pos), "fuel", item)
	end,
	on_unpull_item = function(pos, side, item)
		return tubelib.put_item(M(pos), "main", item)
	end,
	on_recv_message = function(pos, topic, payload)
		if topic == "fuel" then
			return tubelib.fuelstate(M(pos), "fuel")
		end

		local resp = State:on_receive_message(pos, topic, payload)
		if resp then
			return resp
		else
			return "unsupported"
		end
	end,
	on_node_load = function(pos)
		State:on_node_load(pos)
	end,
	on_node_repair = function(pos)
		return State:on_node_repair(pos)
	end,
})


-- update to v0.08
minetest.register_lbm({
	label = "[tubelib_addons1] Harvester update",
	name = "tubelib_addons1:update",
	nodenames = {"tubelib_addons1:harvester_base", "tubelib:harvester_base_active"},
	run_at_every_load = false,
	action = function(pos, node)
		local meta = M(pos)
		local this = minetest.deserialize(meta:get_string("this"))
		if this then
			this.working_pos = this.copter_pos or working_start_pos(pos, this.altitude)
			meta:set_string("this", minetest.serialize(this))
		end
	end
})
