local S = minetest.get_translator("hades_mob_spawner")
local F = minetest.formspec_escape

-- Max. possible light level returned by minetest.get_node_light
local MAX_LIGHT = 15
-- Max. allowed mobs of same type around spawner
local MAX_MOBS_IN_AREA = 10
-- Max. value for player_distance
local MAX_PLAYER_DISTANCE = 20
-- Limits of y_offset
local MIN_Y_OFFSET = -9
local MAX_Y_OFFSET = 9
-- Check for same mobs within this distance from spawner (per-axis)
local SAME_MOB_DIST = 4
-- Spawn mobs up to this many blocks away from spawner
local SPAWN_DIST = 5

local DEFAULT_MIN_LIGHT = 0
local DEFAULT_MAX_LIGHT = MAX_LIGHT
local DEFAULT_MAX_MOBS = 1
local DEFAULT_Y_OFFSET = 0
local DEFAULT_PLAYER_DISTANCE = 0

local function show_spawner_formspec(pos, player_name)
	local meta = minetest.get_meta(pos)
	local mob = meta:get_string("mob")
	local mlig = meta:get_int("min_light")
	local xlig = meta:get_int("max_light")
	local num = meta:get_int("max_mobs")
	local pla = meta:get_float("player_distance")
	local yof = meta:get_float("y_offset")

	-- text entry formspec
	local form = "formspec_version[6]size[6,9.5]"
		.. "label[0.2,0.5;" .. F(S("Mob Spawner")) .. "]"

	local smdist = (SAME_MOB_DIST*2)+1
	local fields = {
		-- { field ID, label, current value, tooltip }
		{ "mob", S("Mob identifier"), mob, S("Identifier of mob to spawn") },
		{ "min_light", S("Minimum light"), mlig, S("Minimum required light level for spawning (0-@1)", MAX_LIGHT) },
		{ "max_light", S("Maximum light"), xlig, S("Maximum allowed light level for spawning (0-@1)", MAX_LIGHT) },
		{ "max_mobs", S("Maximum number of nearby mobs"), num,
			S("Mobs will only spawn if fewer than this number of mobs of the same type are within @1×@2×@3 blocks around the spawner", smdist, smdist, smdist)
		},
		{ "player_distance", S("Required player distance (0=disable)"), pla, S("Mobs will only spawn if at least 1 player is within the chosen distance. The value 0 disables this restriction.") },
		{ "y_offset", S("Height offset"), yof, S("Mobs will spawn this many blocks above the mob spawner (can be negative)") },
	}
	local y = 1.5
	for f=1, #fields do
		local field = fields[f]
		local did_dropdown = false
		if field[1] == "mob" then
			-- Dropdown of all known mobs
			local mob_entries = { "" }
			for k,v in pairs(mobs.spawning_mobs) do
				table.insert(mob_entries, F(k))
			end
			table.sort(mob_entries)
			local idx
			for m=1, #mob_entries do
				if mob_entries[m] == mob then
					idx = m
					break
				end
			end
			-- Show dropdown if current mob is part of the available entries,
			-- otherwise fallback to the freeform text
			if idx then
				did_dropdown = true
				local mob_entries_form = table.concat(mob_entries, ",")
				form = form .. "label[0.5,"..(y-0.15)..";"..F(S("Mob")) .. "]"
				form = form .. "dropdown[0.5,"..y..";5,0.7;"..field[1]..";"..mob_entries_form..";"..idx..";false]"
			end
		end
		if not did_dropdown then
			-- Text input field
			form = form .. "field[0.5,"..y..";5,0.7;"..field[1]..";"..F(field[2])..";"..F(field[3]).."]"
		end
		form = form .. "tooltip["..field[1]..";"..F(field[4]).."]"
		y = y + 1.1
	end
	y = y + 0.2
	form = form .. "button_exit[0.5,"..y..";2,0.7;okay;"..F(S("Okay")).."]"
	form = form .. "button_exit[3.5,"..y..";2,0.7;cancel;"..F(S("Cancel")).."]"

	local pos_str = pos.x.."_"..pos.y.."_"..pos.z
	minetest.show_formspec(player_name, "hades_mob_spawner:spawner_"..pos_str, form)
end

local function update_spawner_node(pos)
	local node = minetest.get_node(pos)
	local meta = minetest.get_meta(pos)
	local mob = meta:get_string("mob")
	local num = meta:get_int("max_mobs")

	local mob_exists = mobs.spawning_mobs[mob] ~= nil or minetest.registered_entities[mob] ~= nil

	if mob == "" or num == 0 or not mob_exists then
		meta:set_string("infotext", S("Mob spawner inactive"))
		if node.name ~= "hades_mob_spawner:spawner_inactive" then
			minetest.swap_node(pos, {name="hades_mob_spawner:spawner_inactive"})
		end
	else
		meta:set_string("infotext", S("Mob spawner active"))
		if node.name ~= "hades_mob_spawner:spawner_active" then
			minetest.swap_node(pos, {name="hades_mob_spawner:spawner_active"})
		end
	end
end

-- Inactive mob spawner
minetest.register_node("hades_mob_spawner:spawner_inactive", {
	description = S("Mob Spawner (inactive)"),
	_tt_help = S("Periodically spawns mobs around it").."\n"..
		S("Spawn area: @1×@2×@3", (SPAWN_DIST*2)+1, 1, (SPAWN_DIST*2)+1),
	tiles = {"hades_mob_spawner_spawner_inactive.png"},
	drawtype = "normal",
	walkable = true,
	groups = {mob_spawner = 1, cracky = 1, not_in_creative_inventory = 1},
	is_ground_content = false,
	sounds = hades_sounds.node_sound_stone_defaults(),
	on_construct = function(pos)
		local meta = minetest.get_meta(pos)

		-- Set other initial settings
		meta:set_int("min_light", DEFAULT_MIN_LIGHT)
		meta:set_int("max_light", DEFAULT_MAX_LIGHT)
		meta:set_int("max_mobs", DEFAULT_MAX_MOBS)
		meta:set_float("player_distance", DEFAULT_PLAYER_DISTANCE)
		meta:set_float("y_offset", DEFAULT_Y_OFFSET)

		update_spawner_node(pos)
	end,
	on_rightclick = function(pos, node, clicker)
		local name = clicker:get_player_name()
		local privs = minetest.get_player_privs(name)
		if not privs.server and not privs.creative then
			minetest.chat_send_player(name, S("You need the “server” or “creative” privilege to change mob spawners!"))
			return
		end

		show_spawner_formspec(pos, name)
	end,
	drop = "hades_mob_spawner:spawner_active",
})

-- Active mob spawner
local sp_def = table.copy(minetest.registered_nodes["hades_mob_spawner:spawner_inactive"])
sp_def.description = S("Mob Spawner")
sp_def.tiles = { "hades_mob_spawner_spawner_active.png" }
sp_def.node_placement_prediction = "hades_mob_spawner:spawner_inactive"
sp_def.groups.not_in_creative_inventory = nil
sp_def.drop = nil
minetest.register_node("hades_mob_spawner:spawner_active", sp_def)

minetest.register_on_player_receive_fields(function(sender, formname, fields)
	local pos
	local sub = string.sub(formname, 1, 26)
	if sub == "hades_mob_spawner:spawner_" then
		sub = string.sub(formname, 27, -1)
		local x, y, z = string.match(sub, "(-?[0-9]+)_(-?[0-9]+)_(-?[0-9]+)")
		x = tonumber(x)
		y = tonumber(y)
		z = tonumber(z)
		if not x or not y or not z then
			return
		end
		pos = vector.new(x,y,z)
	else
		return
	end

	if not fields.mob or not fields.min_light or not fields.max_light or not fields.max_mobs or not fields.player_distance or not fields.y_offset then
		return
	end
	if fields.cancel then
		return
	end
	if not fields.okay and not fields.key_enter then
		return
	end

	local name = sender:get_player_name()
	if minetest.is_protected(pos, name) then
		minetest.record_protection_violation(pos, name)
		return
	end
	local privs = minetest.get_player_privs(name)
	if not privs.server and not privs.creative then
		minetest.chat_send_player(name, S("You need the “server” or “creative” privilege to change mob spawners!"))
		return
	end

	local meta = minetest.get_meta(pos)
	local mob = fields.mob or ""
	local mlig = tonumber(fields.min_light)
	local xlig = tonumber(fields.max_light)
	local num = tonumber(fields.max_mobs)
	local pla = tonumber(fields.player_distance)
	local yof = tonumber(fields.y_offset)

	local fail = false
	local fail_msg
	if mob ~= "" and not (mobs.spawning_mobs[mob]) then
		fail = true
		fail_msg = S("Unknown mob: @1", tostring(mob))
	elseif not mlig then
		fail = true
		fail_msg = S("Minimum light is not a number")
	elseif not xlig then
		fail = true
		fail_msg = S("Maximum light is not a number")
	elseif mlig > xlig then
		fail = true
		fail_msg = S("Minimum light is greater than maximum light")
	elseif not num then
		fail = true
		fail_msg = S("Maximum mobs is not a number")
	elseif not pla then
		fail = true
		fail_msg = S("Player distance is not number")
	elseif not yof then
		fail = true
		fail_msg = S("Y offset is not a number")
	end
	if fail then
		minetest.chat_send_player(name, minetest.colorize("#FF8900", S("Invalid mob spawner configuration: @1", fail_msg)))
		return
	end

	mlig = math.floor(mlig)
	xlig = math.floor(xlig)
	num = math.floor(num)

	mlig = math.max(0, math.min(mlig, MAX_LIGHT))
	xlig = math.max(0, math.min(xlig, MAX_LIGHT))
	num = math.max(0, math.min(num, MAX_MOBS_IN_AREA))
	pla = math.max(0, math.min(pla, MAX_PLAYER_DISTANCE))
	yof = math.max(MIN_Y_OFFSET, math.min(yof, MAX_Y_OFFSET))

	meta:set_string("mob", mob)
	meta:set_int("min_light", mlig)
	meta:set_int("max_light", xlig)
	meta:set_int("max_mobs", num)
	meta:set_float("player_distance", pla)
	meta:set_float("y_offset", yof)

	update_spawner_node(pos)
end)

local max_per_block = tonumber(minetest.settings:get("max_objects_per_block") or 99)

-- spawner ABM
minetest.register_abm({
	label = "Mob spawner spawns mobs",
	nodenames = {"hades_mob_spawner:spawner_active"},
	interval = 10,
	chance = 4,
	catch_up = false,

	action = function(pos, node, active_object_count, active_object_count_wider)

		-- hard cancel if there are too many entities overall nearby
		if active_object_count_wider >= max_per_block then
			return
		end

		-- get meta
		local meta = minetest.get_meta(pos)

		-- get settings from meta
		local mob = meta:get_string("mob")
		local mlig = meta:get_int("min_light")
		local xlig = meta:get_int("max_light")
		local num = meta:get_int("max_mobs")
		local pla = meta:get_float("player_distance")
		local yof = meta:get_float("y_offset")
		num = math.min(num, MAX_MOBS_IN_AREA)
		pla = math.min(pla, MAX_PLAYER_DISTANCE)
		yof = math.min(math.max(yof, MIN_Y_OFFSET), MAX_Y_OFFSET)

		-- if max_mobs is 0, disable spawner and do nothing
		if num == 0 then
			update_spawner_node(pos)
			return
		end

		-- are we spawning a registered mob?
		if not mobs.spawning_mobs[mob] then
			update_spawner_node(pos)
			return
		elseif not minetest.registered_entities[mob] then
			update_spawner_node(pos)
			return
		end

		-- check objects inside a cube around the spawner
		-- for the same-mob check
		local same_mob_offset = vector.new(SAME_MOB_DIST, SAME_MOB_DIST, SAME_MOB_DIST)
		local objs = minetest.get_objects_in_area(vector.subtract(pos, same_mob_offset), vector.add(pos, same_mob_offset))
		local same_count = 0
		local ent

		-- count mob objects of same type in area
		for _, obj in ipairs(objs) do
			ent = obj:get_luaentity()
			if ent and ent.name and ent.name == mob then
				same_count = same_count + 1
			end
		end

		-- are there too many mobs of same type nearby?
		if same_count >= num then
			return
		end

		-- spawn mob if player detected and in range
		if pla > 0 then
			local in_range = false
			local objsp = minetest.get_objects_inside_radius(pos, pla)
			for _, oir in pairs(objsp) do
				if oir:is_player() then
					in_range = true
					break
				end
			end
			-- player not found
			if not in_range then
				return
			end
		end

		-- find air blocks within 5 nodes of spawner
		local air = minetest.find_nodes_in_area(
			{x = pos.x - SPAWN_DIST, y = pos.y + yof, z = pos.z - SPAWN_DIST},
			{x = pos.x + SPAWN_DIST, y = pos.y + yof, z = pos.z + SPAWN_DIST}, {"air"})

		-- spawn in random air block
		if air and #air > 0 then

			local pos2 = air[math.random(#air)]
			local lig = minetest.get_node_light(pos2) or 0

			-- only if light levels are within range
			if lig >= mlig and lig <= xlig then
				minetest.log("action", "[hades_mob_spawner] Mob spawner at "..minetest.pos_to_string(pos).." spawns mob '"..tostring(mob).."' at "..minetest.pos_to_string(pos2))
				pos2.y = pos2.y - 0.5
				minetest.add_entity(pos2, mob)
			end
		end
	end,
})

minetest.register_lbm({
	label = "Update mob spawner states",
	name = "hades_mob_spawner:update_state",
	nodenames = {"group:mob_spawner"},
	run_at_every_load = true,
	bulk_action = function(pos_list)
		for p=1, #pos_list do
			update_spawner_node(pos_list[p])
		end
	end,
})

minetest.register_lbm({
	label = "Update legacy Mobs Redo mob spawners",
	name = "hades_mob_spawner:update_legacy_mobs_redo_spawners",
	nodenames = { "mobs:spawner" },
	action = function(pos)
                local meta = minetest.get_meta(pos)
		local full_command = meta:get_string("command")
                local comm = full_command:split(" ")

                local mob = comm[1] or "" -- mob to spawn
                local mlig = tonumber(comm[2]) or 0 -- min light
                local xlig = tonumber(comm[3]) or MAX_LIGHT -- max light
                local num = tonumber(comm[4]) or DEFAULT_MAX_MOBS -- total mobs in area
                local pla = tonumber(comm[5]) or DEFAULT_PLAYER_DISTANCE -- player distance (0 to disable)
                local yof = tonumber(comm[6]) or DEFAULT_Y_OFFSET -- Y offset to spawn mob

		meta:set_string("mob", mob)
		meta:set_int("min_light", mlig)
		meta:set_int("max_light", xlig)
		meta:set_int("max_mobs", num)
		meta:set_float("player_distance", pla)
		meta:set_float("y_offset", yof)

		meta:set_string("formspec", "")

		update_spawner_node(pos)

		minetest.log("action", "[hades_mob_spawner] Updated legacy Mobs Redo mob spawner at "..minetest.pos_to_string(pos))
	end,
})
