--[[

	Minecart
	========

	Copyright (C) 2019-2020 Joachim Stolberg

	MIT
	See license.txt for more information
	
]]--

-- for lazy programmers
local M = minetest.get_meta
local P2S = function(pos) if pos then return minetest.pos_to_string(pos) end end
local S2P = minetest.string_to_pos
local P2H = minetest.hash_node_position
local H2P = minetest.get_position_from_hash

local param2_to_dir = {[0]=
	{x=0,  y=0,  z=1},
	{x=1,  y=0,  z=0},
	{x=0,  y=0, z=-1},
	{x=-1, y=0,  z=0},
	{x=0,  y=-1, z=0},
	{x=0,  y=1,  z=0}
}

-- Registered carts
minecart.tNodeNames = {} -- [<cart_node_name>] =  <cart_entity_name>
minecart.tEntityNames = {} -- [<cart_entity_name>] =  true
minecart.lCartNodeNames = {} -- {<cart_node_name>, <cart_node_name>, ...}
minecart.tCartTypes = {}

function minecart.param2_to_dir(param2)
	return param2_to_dir[param2 % 6]
end

function minecart.get_node_lvm(pos)
	local node = minetest.get_node_or_nil(pos)
	if node then
		return node
	end
	local vm = minetest.get_voxel_manip()
	local MinEdge, MaxEdge = vm:read_from_map(pos, pos)
	local data = vm:get_data()
	local param2_data = vm:get_param2_data()
	local area = VoxelArea:new({MinEdge = MinEdge, MaxEdge = MaxEdge})
	local idx = area:indexp(pos)
	if data[idx] and param2_data[idx] then
		return {
			name = minetest.get_name_from_content_id(data[idx]),
			param2 = param2_data[idx]
		}
	end
	return {name="ignore", param2=0}
end

function minecart.find_node_near_lvm(pos, radius, items)
	local npos = minetest.find_node_near(pos, radius, items)
	if npos then
		return npos
	end
	local tItems = {}
	for _,v in ipairs(items) do
		tItems[v] = true
	end
	local pos1 = {x = pos.x - radius, y = pos.y - radius, z = pos.z - radius}
	local pos2 = {x = pos.x + radius, y = pos.y + radius, z = pos.z + radius}
	local vm = minetest.get_voxel_manip()
	local MinEdge, MaxEdge = vm:read_from_map(pos1, pos2)
	local data = vm:get_data()
	local area = VoxelArea:new({MinEdge = MinEdge, MaxEdge = MaxEdge})
	for x = pos1.x, pos2.x do
		for y = pos1.y, pos2.y do
			for z = pos1.z, pos2.z do
				local idx = area:indexp({x = x, y = y, z = z})
				local name = minetest.get_name_from_content_id(data[idx])
				if name and tItems[name] then
					return {x = x, y = y, z = z}
				end
			end
		end
	end
end

-- Marker entities for debugging purposes
function minecart.set_marker(pos, text, size, ttl)
	local marker = minetest.add_entity(pos, "minecart:marker_cube")
	if marker ~= nil then
		marker:set_nametag_attributes({color = "#FFFFFF", text = text})
		size = size or 1
		marker:set_properties({visual_size = {x = size, y = size}})
		if ttl then
			minetest.after(ttl, marker.remove, marker)
		end
	end
end

minetest.register_entity("minecart:marker_cube", {
	initial_properties = {
		visual = "cube",
		textures = {
			"minecart_marker_cube.png",
			"minecart_marker_cube.png",
			"minecart_marker_cube.png",
			"minecart_marker_cube.png",
			"minecart_marker_cube.png",
			"minecart_marker_cube.png",
		},
		physical = false,
		visual_size = {x = 1, y = 1},
		collisionbox = {-0.25,-0.25,-0.25, 0.25,0.25,0.25},
		glow = 8,
		static_save = false,
	},
	on_punch = function(self)
		self.object:remove()
	end,
})

function minecart.set_land_marker(pos, radius, ttl)
	local offs = radius + 0.5
	local posses = {
		{x = pos.x + offs, y = pos.y, z=pos.z},
		{x = pos.x, y = pos.y, z=pos.z + offs},
		{x = pos.x - offs, y = pos.y, z=pos.z},
		{x = pos.x, y = pos.y, z=pos.z - offs},
	}
	for i, pos in ipairs(posses) do
		local marker = minetest.add_entity(pos, "minecart:marker")
		if marker ~= nil then
			marker:set_properties({
				visual_size = {x = 2 * offs, y = 2 * offs},
				collisionbox = {-offs, -offs, 0, offs, offs, 0},
			})
			marker:set_yaw(math.pi / 2 * i)
			minetest.after(ttl, marker.remove, marker)
		end
	end
end

minetest.register_entity("minecart:marker", {
	initial_properties = {
		visual = "upright_sprite",   
		textures = {"minecart_marker_cube.png"},
		use_texture_alpha = true,     
		physical = false,
		glow = 12,
		static_save = false,
	},
	on_punch = function(self)
		self.object:remove()
	end,
})

function minecart.is_air_like(name)
	local ndef = minetest.registered_nodes[name]
	if ndef and ndef.buildable_to then
		return true
	end
	return false
end

function minecart.range(val, min, max)
	val = tonumber(val)
	if val < min then return min end
	if val > max then return max end
	return val
end

function minecart.get_next_node(pos, param2)
	local pos2 = param2 and vector.add(pos, param2_to_dir[param2]) or pos
	local node = minetest.get_node(pos2)
	return pos2, node
end

function minecart.get_object_id(object)
	for id, entity in pairs(minetest.luaentities) do
		if entity.object == object then
			return id
		end
	end
end

function minecart.is_owner(player, owner)
	if not player or not player:is_player() or not owner or owner == "" then
		return true
	end

	local name = player:get_player_name()
	if minetest.check_player_privs(name, "minecart") then
		return true
	end
	return name == owner
end

function minecart.get_buffer_pos(pos, player_name)
	local pos1 = minecart.find_node_near_lvm(pos, 1, {"minecart:buffer"})
	if pos1 then
		local meta = minetest.get_meta(pos1)
		if player_name == nil or player_name == meta:get_string("owner") then
			return pos1
		end
	end
end

function minecart.get_buffer_name(pos)
	local pos1 = minecart.find_node_near_lvm(pos, 1, {"minecart:buffer"})
	if pos1 then
		local name = M(pos1):get_string("name")
		if name ~= "" then
			return name
		end
		return P2S(pos1)
	end
end

function minecart.manage_attachment(player, entity, get_on)
	if not player then
		return
	end
	local player_name = player:get_player_name()
	if player_api.player_attached[player_name] == get_on then
		return
	end
	player_api.player_attached[player_name] = get_on
	
	local obj = entity.object
	if get_on then
		player:set_attach(obj, "", {x=0, y=-4.5, z=-4}, {x=0, y=0, z=0})
		player:set_eye_offset({x=0, y=-6, z=0},{x=0, y=-6, z=0})
		player:set_properties({visual_size = {x = 2.5, y = 2.5}})
		player_api.set_animation(player, "sit")
		entity.driver = player:get_player_name()
	else
		player:set_detach()
		player:set_eye_offset({x=0, y=0, z=0},{x=0, y=0, z=0})
		player:set_properties({visual_size = {x = 1, y = 1}})
		player_api.set_animation(player, "stand")
		entity.driver = nil
	end
end

function minecart.register_cart_names(node_name, entity_name, cart_type)
	minecart.tNodeNames[node_name] = entity_name
	minecart.tEntityNames[entity_name] = true
	minecart.lCartNodeNames[#minecart.lCartNodeNames+1] = node_name
	minecart.add_raillike_nodes(node_name)
	minecart.tCartTypes[node_name] = cart_type
end

function minecart.add_nodecart(pos, node_name, param2, cargo, owner, userID)
	if pos and node_name and param2 and cargo and owner and userID then
		local pos2
		if not minecart.is_rail(pos) then
			pos2 = minetest.find_node_near(pos, 1, minecart.lRails)
			if not pos2 or not minecart.is_rail(pos2) then
				-- If no rail is around, use an available cart as new search center
				pos2 = minetest.find_node_near(pos, 1, minecart.lRailsExt)
				-- ...and search again.
				if pos2 then
					pos2 = minetest.find_node_near(pos2, 1, minecart.lRails)
				end
			end
		else
			pos2 = vector.new(pos)
		end
		if pos2 then
			local node = minetest.get_node(pos2)
			local ndef = minetest.registered_nodes[node_name]
			local rail = node.name
			minetest.swap_node(pos2, {name = node_name, param2 = param2})
			local meta = M(pos2)
			meta:set_string("removed_rail", rail)
			meta:set_string("owner", owner)
			meta:set_int("userID", userID)
			meta:set_string("infotext", owner .. ": " .. userID)
			
			if cargo and ndef.set_cargo then
				ndef.set_cargo(pos2, cargo)
			end
			if ndef.after_place_node then
				ndef.after_place_node(pos2)
			end
			return pos2
		end
	end
end

function minecart.add_entitycart(pos, node_name, entity_name, vel, cargo, owner, userID)
	local obj = minetest.add_entity(pos, entity_name)
	local objID = minecart.get_object_id(obj)
	
	if objID then
		local entity = obj:get_luaentity()
		entity.start_pos = pos
		entity.owner = owner
		entity.node_name = node_name
		entity.userID = userID
		entity.objID = objID
		entity.cargo = cargo
		obj:set_nametag_attributes({color = "#ffff00", text = owner..": "..userID})
		obj:set_velocity(vel)
		return obj
	end
end

function minecart.start_entitycart(self, pos, facedir)
	local route = {}
	
	self.is_running = true
	self.arrival_time = 0
	self.start_pos = minecart.get_buffer_pos(pos, self.owner) or minecart.get_next_buffer(pos, facedir)
	if self.start_pos then
		-- Read buffer route for the junction info
		route = minecart.get_route(self.start_pos) or {}
		self.junctions = route and route.junctions
	end
	-- If set the start waypoint will be deleted
	self.no_normal_start = self.start_pos == nil
	if self.driver == nil then
		minecart.start_monitoring(self.owner, self.userID, pos, self.objID, 
				route.checkpoints, route.junctions, self.cargo or {})
	end
end

function minecart.remove_nodecart(pos)
	local node = minetest.get_node(pos)
	local ndef = minetest.registered_nodes[node.name]
	local meta = M(pos)
	local rail = meta:get_string("removed_rail")
	if rail == "" then rail = "air" end
	local userID = meta:get_int("userID")
	local owner = meta:get_string("owner")
	meta:set_string("infotext", "")
	meta:set_string("formspec", "")
	local cargo = ndef.get_cargo and ndef.get_cargo(pos) or {}
	minetest.swap_node(pos, {name = rail})
	return cargo, owner, userID
end	
	
function minecart.node_to_entity(pos, node_name, entity_name)
	-- Remove node
	local cargo, owner, userID = minecart.remove_nodecart(pos)
	local obj = minecart.add_entitycart(pos, node_name, entity_name,
			{x = 0, y = 0, z = 0}, cargo, owner, userID)
	if obj then
		return obj
	else
		print("Entity has no ID")
	end
end

function minecart.entity_to_node(pos, entity)
	-- Stop sound
	if entity.sound_handle then
		minetest.sound_stop(entity.sound_handle)
		entity.sound_handle = nil
	end
	
	local rot = entity.object:get_rotation()
	local dir = minetest.yaw_to_dir(rot.y)
	local facedir = minetest.dir_to_facedir(dir)
	minecart.stop_recording(entity, pos)
	local pos2 = minecart.add_nodecart(pos, entity.node_name, facedir, entity.cargo, entity.owner, entity.userID)
	if pos2 then
		minecart.stop_monitoring(entity.owner, entity.userID, pos2)
		entity.object:remove()
	else
		minecart.start_entitycart(entity, pos, facedir)
	end
end

function minecart.add_node_to_player_inventory(pos, player, node_name)
	local inv = player:get_inventory()
	if not (creative and creative.is_enabled_for
			and creative.is_enabled_for(player:get_player_name()))
			or not inv:contains_item("main", node_name) then
		local leftover = inv:add_item("main", node_name)
		-- If no room in inventory, drop the cart
		if not leftover:is_empty() then
			minetest.add_item(pos, leftover)
		end
	end
end

-- Player removes the node
function minecart.remove_entity(self, pos, player)
	-- Stop sound
	if self.sound_handle then
		minetest.sound_stop(self.sound_handle)
		self.sound_handle = nil
	end
	if player then
		minecart.add_node_to_player_inventory(pos, player, self.node_name or "minecart:cart")
	end
	minecart.stop_monitoring(self.owner, self.userID, pos)
	minecart.stop_recording(self, pos)	
	self.object:remove()
end
