-- list of all players seen by minetest.register_on_joinplayer
local players = {}
-- all player positions last time light was updated: {player_name : {x, y, z}}
local player_positions = {}
-- all light positions of light that currently is created {player_name : {i: {x, y, z}}
local light_positions = {}
-- last item seen wielded by players
local last_wielded = {}

-- toggles debug mode
local walking_light_debug = false
-- name of light node, changed by toggling debug mode
local walking_light_node = nil

-- initialize walking light
walking_light = {}

-- list of items that use walking light
local light_items = {
	"default:torch",
	"walking_light:helmet_diamond", "walking_light:megatorch"
}

function walking_light.addLightItem(item)
	for I in pairs(light_items) do
		if item == light_items[I] then
			minetest.log("warning", "[walking_light] \"" .. item .. "\" is already light item.")
			return
		end
	end
	
	table.insert(light_items, -1, item)
end

function walking_light.getLightItems()
	return light_items
end

function walking_light.register_tool(tool)
	item = 'walking_light:' .. tool .. '_mese'
	default = 'default:' .. tool .. '_mese'

	definition = table.copy(minetest.registered_items[default])
	definition.description = definition.description .. ' with light'
	definition.inventory_image = 'walking_light_mese' .. tool .. '.png'

	minetest.register_tool(item, definition)
	minetest.register_craft({
		output = item,
		recipe = {
			{'default:torch'},
			{ default },
		}
	})

	walking_light.addLightItem(item)
end

-- from http://lua-users.org/wiki/IteratorsTutorial
-- useful for removing things from a table because removing from the middle makes it skip elements otherwise
function ripairs(t)
  local function ripairs_it(t,i)
    i=i-1
    local v=t[i]
    if v==nil then return v end
    return i,v
  end
  return ripairs_it, t, #t+1
end

-- formats a vector with shorter output than dump
local function dumppos(pos)
	if pos == nil then
		return "nil"
	end
	local x = "nil"
	if pos.x then
		x = pos.x
	end
	local y = "nil"
	if pos.y then
		y = pos.y
	end
	local z = "nil"
	if pos.z then
		z = pos.z
	end

	return "(" .. x .. "," .. y .. "," .. z .. ")"
end

-- formats a table containing vectors with shorter output than dump
local function dumppostable(t)
	if t == nil then
		return "nil"
	end
	if #t == 0 then
		return "0{}"
	end

	ret = #t .. "{\n"
	for i,pos in ipairs(t) do
		ret = ret .. "    " .. dumppos(pos) .. "\n"
	end
	ret = ret .. "}"
	return ret
end

function mt_get_node_or_nil(pos)
	if pos == nil then
		print("ERROR: walking_light.mt_get_node_or_nil(), pos is nil")
		print(debug.traceback("Current Callstack:\n"))
		return nil
	end
	return minetest.get_node_or_nil(pos)
end

function mt_add_node(pos, sometable)
	if pos == nil then
		print("ERROR: walking_light.mt_add_node(), pos is nil")
		print(debug.traceback("Current Callstack:\n"))
		return nil
	end
	if sometable == nil then
		print("ERROR: walking_light.mt_add_node(), sometable is nil")
		print(debug.traceback("Current Callstack:\n"))
		return nil
	end
	minetest.add_node(pos,sometable)
end

function round(num) 
	return math.floor(num + 0.5) 
end

local function poseq(pos1, pos2)
	if pos1 == nil and pos2 == nil then
		return true
	end
	if pos1 == nil or pos2 == nil then
		return false
	end
	return pos1.x == pos2.x and pos1.y == pos2.y and pos1.z == pos2.z
end

-- return true if the player moved since last player_positions update
local function player_moved(player)
	local player_name = player:get_player_name()
	local pos = player:getpos()
	local rounded_pos = vector.round(pos)
	local oldpos = player_positions[player_name]
	if oldpos == nil or not poseq(rounded_pos, oldpos) then
		-- if oldpos is nil, we assume they just logged in, so consider them moved
--		print("DEBUG: walking_light, player_moved(); moved = true; rounded_pos = " .. dumppos(rounded_pos) .. ", oldpos = " .. dumppos(oldpos))
		return true
	end
--	print("DEBUG: walking_light, player_moved(); moved = false; rounded_pos = " .. dumppos(rounded_pos) .. ", oldpos = " .. dumppos(oldpos))
	return false
end

-- same as table.remove(t,remove_pos), but uses poseq instead of comparing references (does lua have comparator support, so this isn't needed?)
local function table_remove_pos(t, remove_pos)
--	local DEBUG_oldsize = #t

	for i,pos in ipairs(t) do
		if poseq(pos, remove_pos) then
			table.remove(t, i)
			break
		end
	end

--	local DEBUG_newsize = #t
--	print("DEBUG: walking_light.table_remove_pos(), oldsize = " .. DEBUG_oldsize .. ", newsize = " .. DEBUG_newsize)
end

-- same as t[remove_pos], but uses poseq instead of comparing references (does lua have comparator support, so this isn't needed?)
local function table_contains_pos(t, remove_pos)
	for i,pos in ipairs(t) do
		if poseq(pos, remove_pos) then
			return true
		end
	end
	return false
end

-- same as table.insert(t,pos) but makes sure it is not duplicated
local function table_insert_pos(t, pos)
	if not table_contains_pos( pos ) then
		table.insert(t, pos)
	end
end

local function is_light(node)
	if node ~= nil and ( node.name == "walking_light:light" or node.name == "walking_light:light_debug" ) then
		return true
	end
	return false
end
    
-- removes light at the given position
-- player is optional
local function remove_light(player, pos)
	local player_name
	if player then
		player_name = player:get_player_name()
	end
	local node = mt_get_node_or_nil(pos)
	if is_light(node) then
		mt_add_node(pos,{type="node",name="air"})
		if player_name then
			table_remove_pos(light_positions[player_name], pos)
		end
	else
		if node ~= nil then
			print("WARNING: walking_light.remove_light(), pos = " .. dumppos(pos) .. ", tried to remove light but node was " .. node.name)
			table_remove_pos(light_positions[player_name], pos)
		else
			print("WARNING: walking_light.remove_light(), pos = " .. dumppos(pos) .. ", tried to remove light but node was nil")
		end
	end
end

-- removes all light owned by a player
local function remove_light_player(player)
	local player_name = player:get_player_name()

    for i,old_pos in ripairs(light_positions[player_name]) do
		if old_pos then
--			print("DEBUG: walking_light.remove_light_player(), removing old light; old_pos = " .. dumppos(old_pos))
			remove_light(player, old_pos)
		end
	end
--	print("DEBUG: walking_light.remove_light_player(), done; light_positions = " .. dumppostable(light_positions[player_name]))
end

local function can_add_light(pos)
	local node  = mt_get_node_or_nil(pos)
	if node == nil or node.name == "air" then
--		print("walking_light can_add_light(), pos = " .. dumppos(pos) .. ", true")
		return true
	elseif is_light(node) then
--		print("walking_light can_add_light(), pos = " .. dumppos(pos) .. ", true")
		return true
	end
--	print("walking_light can_add_light(), pos = " .. dumppos(pos) .. ", false")
	return false
end

-- old function returns pos instead of table, for only one position
local function pick_light_position_regular(player, pos)
	if can_add_light(pos) then
		return {pos}
	end

	local pos2

	-- if pos is not possible, try the old player position first, to make it more likely that it has a line of sight
	local player_name = player:get_player_name()
	local oldplayerpos = player_positions[player_name]
	if oldplayerpos and can_add_light( vector.new(oldplayerpos.x, oldplayerpos.y + 1, oldplayerpos.z) ) then
		return oldplayerpos 
	end

	-- if not, try all positions around the pos
	pos2 = vector.new(pos.x + 1, pos.y, pos.z)
	if can_add_light( pos2 ) then
		return {pos2}
	end

	pos2 = vector.new(pos.x - 1, pos.y, pos.z)
	if can_add_light( pos2 ) then
		return {pos2}
	end

	pos2 = vector.new(pos.x, pos.y, pos.z + 1)
	if can_add_light( pos2 ) then
		return {pos2}
	end

	pos2 = vector.new(pos.x, pos.y, pos.z - 1)
	if can_add_light( pos2 ) then
		return {pos2}
	end

	pos2 = vector.new(pos.x, pos.y + 1, pos.z)
	if can_add_light( pos2 ) then
		return {pos2}
	end

	pos2 = vector.new(pos.x, pos.y - 1, pos.z)
	if can_add_light( pos2 ) then
		return {pos2}
	end

	return nil
end

-- new function, returns table
local function pick_light_position_radius(player, pos, ret, radius)
	local pos2
	local step = 4
	local unstep = 1/step

	for x = pos.x - radius, pos.x + radius, step do
		for y = pos.y - radius, pos.y + radius, step do
			for z = pos.z - radius, pos.z + radius, step do
				pos2 = vector.new(round(x*unstep)*step, round(y*unstep)*step, round(z*unstep)*step)
				distance = math.sqrt(math.pow(pos.x - x, 2) + math.pow(pos.y - y, 2) + math.pow(pos.z - z, 2))
				if distance <= radius and can_add_light( pos2 ) then
					table.insert(ret, pos2)
				end
			end
		end
	end

	return ret
end

local function pick_light_position_mega(player, pos)
	local ret = {}

	if can_add_light(pos) then
		table.insert(ret, pos)
	end
	pick_light_position_radius(player, pos, ret, 10)

	return ret
end

local function pick_light_position(player, pos, light_item)
	if light_item == "walking_light:megatorch" then
		return pick_light_position_mega(player, pos)
	end
	return pick_light_position_regular(player, pos)
end

-- adds light at the given position
local function add_light(player, pos)
	local player_name = player:get_player_name()
	local node  = mt_get_node_or_nil(pos)
	if node == nil then
		-- don't do anything for nil blocks... they are non-loaded blocks, so we don't want to overwrite anything there
--		print("DEBUG: walking_light.add_light(), node is nil, pos = " .. dumppos(pos))
		return false
	elseif node.name == "air" then
		-- when the node that is already there is air, add light
		mt_add_node(pos,{type="node",name=walking_light_node})
		if not table_contains_pos(light_positions[player_name], pos) then
			table_insert_pos(light_positions[player_name], pos)
		end

--		if node then
--			print("DEBUG: add_light(), node.name = " .. node.name .. ", pos = " .. dumppos(pos))
--		else
--			print("DEBUG: add_light(), node.name = nil, pos = " .. dumppos(pos))
--		end
		return true
	elseif is_light(node) then
		-- no point in adding light where it is already, but we should assign it to the player so it gets removed (in case it has no player)
--		print("DEBUG: add_light(), not adding; node.name = " .. node.name .. ", pos = " .. dumppos(pos))

		if not table_contains_pos(light_positions[player_name], pos) then
			table_insert_pos(light_positions[player_name], pos)
		end

		return true
	end
--	print("DEBUG: add_light(), not adding; node.name = " .. node.name)
	return false
end

-- updates all the light around the player, depending on what they are wielding
local function update_light_player(player)
	-- if there is no player, there can be no update
	if not player then
		return
	end

	-- figure out if they wield light; this will be nil if not
	local wielded_item = get_wielded_light_item(player)

	local player_name = player:get_player_name()
	local pos = player:getpos()
	local rounded_pos = vector.round(pos)

	-- check for a nil node where the player is; if it is nil, we assume the block is not loaded, so we return without updating player_positions
	-- that way, it should add light next step
	local node  = mt_get_node_or_nil(rounded_pos)
	if node == nil then
		return
	end

	if not player_moved(player) and wielded_item == last_wielded[player_name] then
		-- no update needed if the wiedled light item is the same as before (including nil), and the player didn't move
		return
	end
	last_wielded[player_name] = wielded_item;

	local wantlightpos = nil
	local wantpos = vector.new(rounded_pos.x, rounded_pos.y + 1, rounded_pos.z)
	if wielded_item then
		-- decide where light should be
		wantlightpos = pick_light_position(player, wantpos, wielded_item)
--		print("DEBUG: walking_light update_light_player(); wantpos = " .. dumppos(wantpos) .. ", wantlightpos = " .. dumppos(wantlightpos))
	end

	if wielded_item and wantlightpos then
		-- add light that isn't already there
		for i,newpos in ipairs(wantlightpos) do
			add_light(player, newpos)
		end
	end

	-- go through all light owned by the player to remove all but what should be kept
    for i,oldlightpos in ripairs(light_positions[player_name]) do
        if not wantlightpos or oldlightpos and oldlightpos.x and not table_contains_pos(wantlightpos, oldlightpos) then
			remove_light(player, oldlightpos)
        end
    end

	player_positions[player_name] = vector.round(pos)

--	print("DEBUG: walking_light.update_light_player(): wantlightpos = " .. dumppostable(wantlightpos) .. ", light_positions = " .. dumppostable(light_positions[player_name]))
end

local function update_light_all()
	-- go through all players to check
	for i,player_name in ipairs(players) do
		local player = minetest.get_player_by_name(player_name)
		update_light_player(player)
	end
end

-- return true if item is a light item
function is_light_item(item)
	for I in pairs(light_items) do
		if item == light_items[I] then
			return true
		end
	end
	return false
end

-- returns a string, the name of the item found that is a light item
function get_wielded_light_item(player)
	local wielded_item = player:get_wielded_item():get_name()
	if is_light_item(wielded_item) then
		return wielded_item
	end

	-- check equipped armor - requires unified_inventory maybe
	local player_name = player:get_player_name()
	if player_name then
		local armor_inv = minetest.get_inventory({type="detached", name=player_name.."_armor"})
		if armor_inv then
--            print( dump(armor_inv:get_lists()) )
			local item_name = "walking_light:helmet_diamond"
			local stack = ItemStack(item_name)
			if armor_inv:contains_item("armor", stack) then
				return item_name
			end
		end
	end

	return nil
end

-- return true if player is wielding a light item
function wielded_light(player)
	return get_wielded_light_item(player) ~= nil
end

minetest.register_on_joinplayer(function(player)
	local player_name = player:get_player_name()
	table.insert(players, player_name)
	last_wielded[player_name] = get_wielded_light_item(player)
	local pos = player:getpos()
	player_positions[player_name] = nil
	light_positions[player_name] = {}
	update_light_player(player)
end)

minetest.register_on_leaveplayer(function(player)
	local player_name = player:get_player_name()
	for i,v in ipairs(players) do
		if v == player_name then
			table.remove(players, i)
		end
	end
	last_wielded[player_name] = false
	remove_light_player(player)
	player_positions[player_name]=nil
end)

minetest.register_globalstep(function(dtime)
	for i,player_name in ipairs(players) do
		local player = minetest.get_player_by_name(player_name)
		if player ~= nil then
			update_light_player(player)
		else
			table.remove(players, i)
		end
	end
end)

minetest.register_node("walking_light:light_debug", {
	drawtype = "glasslike",
	tiles = {"walking_light_debug.png"},
	inventory_image = minetest.inventorycube("walking_light.png"),
	paramtype = "light",
	walkable = false,
	is_ground_content = true,
	sunlight_propagates = true,
	light_source = 14,
	selection_box = {
		type = "fixed",
		fixed = {0, 0, 0, 0, 0, 0},
	},
})

minetest.register_node("walking_light:light", {
	drawtype = "glasslike",
	tiles = {"walking_light.png"},
	inventory_image = minetest.inventorycube("walking_light.png"),
	paramtype = "light",
	walkable = false,
	is_ground_content = true,
	sunlight_propagates = true,
	light_source = 14,
	selection_box = {
		type = "fixed",
		fixed = {0, 0, 0, 0, 0, 0},
	},
})

function update_walking_light_node()
	if walking_light_debug then
		walking_light_node = "walking_light:light_debug"
	else
		walking_light_node = "walking_light:light"
	end
end
update_walking_light_node()

walking_light.register_tool('pick')
walking_light.register_tool('axe')
walking_light.register_tool('shovel')
walking_light.register_tool('sword')

minetest.register_tool("walking_light:helmet_diamond", {
	description = "Diamond Helmet with light",
	inventory_image = "walking_light_inv_helmet_diamond.png",
	wield_image = "3d_armor_inv_helmet_diamond.png",
	groups = {armor_head=15, armor_heal=12, armor_use=100},
	wear = 0,
})

minetest.register_node("walking_light:megatorch", {
    description = "Megatorch",
    drawtype = "torchlike",
    tiles = {
        {
            name = "default_torch_on_floor_animated.png",
            animation = {
                type = "vertical_frames",
                aspect_w = 16,
                aspect_h = 16,
                length = 3.0
            },
        },
        {
            name="default_torch_on_ceiling_animated.png",
            animation = {
                type = "vertical_frames",
                aspect_w = 16,
                aspect_h = 16,
                length = 3.0
            },
        },
        {
            name="default_torch_animated.png",
            animation = {
                type = "vertical_frames",
                aspect_w = 16,
                aspect_h = 16,
                length = 3.0
            },
        },
    },
    inventory_image = "default_torch_on_floor.png",
    wield_image = "default_torch_on_floor.png",
    paramtype = "light",
    paramtype2 = "wallmounted",
    sunlight_propagates = true,
    is_ground_content = false,
    walkable = false,
    light_source = 14,
    selection_box = {
        type = "wallmounted",
        wall_top = {-0.1, 0.5-0.6, -0.1, 0.1, 0.5, 0.1},
        wall_bottom = {-0.1, -0.5, -0.1, 0.1, -0.5+0.6, 0.1},
        wall_side = {-0.5, -0.3, -0.1, -0.5+0.3, 0.3, 0.1},
    },
    groups = {choppy=2,dig_immediate=3,flammable=1,attached_node=1},
    legacy_wallmounted = true,
    --sounds = default.node_sound_defaults(),
})

minetest.register_craft({
	output = 'walking_light:helmet_diamond',
	recipe = {
		{'default:torch'},
		{'3d_armor:helmet_diamond'},
	}
})

minetest.register_craft({
	output = 'walking_light:megatorch',
	recipe = {
		{'default:torch', 'default:torch', 'default:torch'},
		{'default:torch', 'default:torch', 'default:torch'},
		{'default:torch', 'default:torch', 'default:torch'},
	}
})

minetest.register_chatcommand("walking_light_clear_light", {
	params = "<size>",
	description = "Remove light nodes from the area",
	func = function(name, param)
		if not minetest.check_player_privs(name, {server=true}) then
			return false, "You need the server privilege to use mapclearlight"
		end

		local pos = vector.round(minetest.get_player_by_name(name):getpos())
		local size = tonumber(param) or 40

		for i,v in ipairs({"walking_light:light", "walking_light:light_debug"}) do
			point = minetest.find_node_near(pos, size/2, v)
			while point do
				remove_light(nil, point)
				oldpoint = point
				point = minetest.find_node_near(pos, size/2, v)
				if poseq(oldpoint, point) then
					return false, "Failed... infinite loop detected"
				end
			end
		end
		return true, "Done."
	end,
})

minetest.register_chatcommand("walking_light_add_light", {
	params = "<size>",
	description = "Add walking_light:light to a position, without a player owning it",
	func = function(name, param)
		if not minetest.check_player_privs(name, {server=true}) then
			return false, "You need the server privilege to use mapaddlight"
		end

		local pos = vector.round(minetest.get_player_by_name(name):getpos())
		pos = vector.new(pos.x, pos.y + 1, pos.z)

		if pos then
			mt_add_node(pos,{type="node",name=walking_light_node})
		end

		return true, "Done."
	end,
})

minetest.register_chatcommand("walking_light_debug", {
	description = "Change to debug mode, so light blocks are visible.",
	func = function(name, param)
		if not minetest.check_player_privs(name, {server=true}) then
			return false, "You need the server privilege to use walking_light_debug"
		end

		walking_light_debug = not walking_light_debug
        update_walking_light_node()
 
		return true, "Done."
	end,
})

-- vim: ts=4 sw=4 softtabstop=4 smarttab noexpandtab
