--[[

	Hyperloop Mod
	=============

	Copyright (C) 2017-2019 Joachim Stolberg

	LGPLv2.1+
	See LICENSE.txt for more information

	Station and elevator network management
]]--

-- for lazy programmers
local S = function(pos) if pos then return minetest.pos_to_string(pos) end end
local P = minetest.string_to_pos
--local M = minetest.get_meta

-- Convert to list and add pos based on key string
local function table_to_list(table)
	local lRes = {}
	for key,item in pairs(table) do
		item.pos = P(key)
		lRes[#lRes+1] = item
	end
	return lRes
end

local function distance(pos1, pos2)
	return math.floor(math.abs(pos1.x - pos2.x) +
			math.abs(pos1.y - pos2.y) + math.abs(pos1.z - pos2.z))
end

-- Add the distance to pos to each list item
local function add_distance_to_list(lStations, pos)
	for _,item in ipairs(lStations) do
		item.distance = distance(item.pos, pos)
	end
	return lStations
end

-- Add the index to each list item
local function add_index_to_list(lStations)
	-- walk through the list of floors for the next connection
	local get_next = function(key, idx)
		for _,floor in ipairs(lStations) do
			if floor.conn[6] == key then  -- upward match?
				floor.idx = idx
				return S(floor.pos) -- return floor key
			end
		end
	end

	local key = nil
	for idx = 1,#lStations do
		key = get_next(key, idx)
	end
	return lStations
end

-- Return a table with all stations, the given station (as 'sKey') is connected with
-- tRes is used for the resulting table (recursive call)
local function get_stations(tStations, sKey, tRes)
	if not tStations[sKey] or not tStations[sKey].conn then
		return {}
	end
	for dir,dest in pairs(tStations[sKey].conn) do
		-- Not already visited?
		if not tRes[dest] then
			-- Known station?
			if tStations[dest] then
				tStations[dest].name = tStations[dest].name or ""
				tRes[dest] = tStations[dest]
				get_stations(tStations, dest, tRes)
			end
		end
	end
	return tRes
end

-- Return a list with sorted elevators, beginning with the top car
-- with no shaft upwards
local function sort_based_on_level(tStations)
	local lStations = table_to_list(table.copy(tStations))
	-- to be able to sort the list, an index has to be added
	lStations = add_index_to_list(lStations)
	table.sort(lStations, function(a,b) return (a.idx or 9999) < (b.idx or 9999) end)
	return lStations
end

-- Return a list with sorted stations
local function sort_based_on_distance(tStations, pos)
	local lStations = table_to_list(table.copy(tStations))
	-- to be able to sort the list, the distance to pos has to be added
	lStations = add_distance_to_list(lStations, pos)
	table.sort(lStations, function(a,b) return a.distance < b.distance end)
	return lStations
end

-- Return a list with sorted stations
local function sort_based_on_name(tStations, pos)
	local lStations = table_to_list(table.copy(tStations))
	-- Add distance
	lStations = add_distance_to_list(lStations, pos)
	table.sort(lStations, function(a,b) return a.name < b.name end)
	return lStations
end


--
-- Class Network
--

--[[
	tStations["(x,y,z)"] = {
		["conn"] = {
			dir = "(200,0,20)",
		},
	}
	change_counter = n,
]]--


local Network = {}
hyperloop.Network = Network

function Network:new()
	local o = {
		tStations = {},
		change_counter = 0,
	}
	setmetatable(o, self)
	self.__index = self
	return o
end

-- Set an elevator or station entry.
-- tAttr is a table with additional attributes to be stored.
function Network:set(pos, name, tAttr)
	if pos then
		local sKey = S(pos)
		if not self.tStations[sKey] then
			self.tStations[sKey] = {
				conn = {},
			}
		end
		self.tStations[sKey].name = name or ""
		for k,v in pairs(tAttr) do
			self.tStations[sKey][k] = v
		end
		self.change_counter = self.change_counter + 1
	end
end

-- Update an elevator or station entry.
-- tAttr is a table with additional attributes to be stored.
function Network:update(pos, tAttr)
	if pos then
		local sKey = S(pos)
		if self.tStations[sKey] then
			for k,v in pairs(tAttr) do
				if v == "nil" then
					self.tStations[sKey][k] = nil
				else
					self.tStations[sKey][k] = v
				end
			end
			self.change_counter = self.change_counter + 1
		end
	end
end

function Network:get(pos)
	return pos and self.tStations[S(pos)]
end

-- Delete an elevator or station entry.
function Network:delete(pos)
	if pos then
		self.tStations[S(pos)] = nil
		self.change_counter = self.change_counter + 1
	end
end

function Network:changed(counter)
	return self.change_counter > counter, self.change_counter
end

-- Update the connection data base. The output dir information is needed
-- to be able to delete a connection, if necessary.
-- Returns true, if data base is changed.
function Network:update_connections(pos, out_dir, conn_pos)
	local sKey = S(pos)
	local res = false
	if not self.tStations[sKey] then
		self.tStations[sKey] = {}
		res = true
	end
	if not self.tStations[sKey].conn then
		self.tStations[sKey].conn = {}
		res = true
	end
	conn_pos = S(conn_pos)
	if self.tStations[sKey].conn[out_dir] ~= conn_pos then
		self.tStations[sKey].conn[out_dir] = conn_pos
		res = true
	end
	if res then
		self.change_counter = self.change_counter + 1
	end
	return res
end

-- Return the nearest station position
function Network:get_next_station(pos)
	local min_dist = 999999
	local min_key = nil
	local dist
	for key,item in pairs(self.tStations) do
		if not item.junction then
			dist = distance(pos, P(key))
			if dist < min_dist then
				min_dist = dist
				min_key = key
			end
		end
	end
	return P(min_key)
end

-- Return a sorted list of stations
-- Param pos: player pos
-- Param station_pos: next station pos or nil.
--                    Used to generate list with connected stations only
-- Param sorted: either "dist" or "level"
function Network:station_list(pos, station_pos, sorted)
	local tStations, lStations
	if station_pos then
		local tRes = {}
		tStations = get_stations(self.tStations, S(station_pos), tRes) -- reduced
	else
		tStations = self.tStations  -- all stations
	end
	if sorted == "dist" then
		lStations = sort_based_on_distance(tStations, pos)
	elseif sorted == "level" then
		lStations = sort_based_on_level(tStations)
	else
		-- delete own station from list
		tStations[S(station_pos)] = nil
		lStations = sort_based_on_name(tStations, pos)
	end
	return lStations
end

-- Check the complete table by means of the provided callback bool = func(pos)
function Network:filter(callback)
	local lKeys = {}
	for key,_ in pairs(self.tStations) do
		lKeys[#lKeys+1] = key
	end
	for _,key in ipairs(lKeys) do
		if not callback(P(key)) then
			self.tStations[key] = nil
		end
	end
end

function Network:deserialize(data)
	if data ~= "" then
		data = minetest.deserialize(data)
		self.tStations = data.tStations
		self.change_counter = data.change_counter
	end
end

function Network:serialize()
	return minetest.serialize(self)
end

-- Return a pos/item table with all network nodes, the node at pos is connected with
function Network:get_node_table(pos)
	local tRes = {}
	local key = S(pos)
	get_stations(self.tStations, key, tRes)
	tRes[key] = nil
	return tRes
end
