-- LUALOCALS < ---------------------------------------------------------
local core, dump, math, pairs, select, string, table, tonumber, type,
      unpack
    = core, dump, math, pairs, select, string, table, tonumber, type,
      unpack
local math_random, string_format, string_gsub, table_concat
    = math.random, string.format, string.gsub, table.concat
-- LUALOCALS > ---------------------------------------------------------

------------------------------------------------------------------------
-- Low-level layer basic utilities (low dependencies)
------------------------------------------------------------------------

local modname = core.get_current_modname()
local util = {}

------------------------------------------------------------------------
-- Optional Chaining & Deep Structure Access

-- Optional chaining operator that fetches a value nested in a structure
-- but returns nil if the structure, or any key along the path, is absent.

-- getpath(obj, a, b, c) is equivalent to obj?.[a]?.[b]?.[c] in JS:
-- it walks obj[a][b][c], returning nil as soon as any level is nil.
-- Unlike 'obj and obj.a and obj.a.b and obj.a.b.c' in Lua, only nil is
-- treated as missing; if an intermediate value is literally false, we
-- will still attempt to index it and raise an error.
do
	local function getpath(obj, key, ...)
		if obj == nil or key == nil then return obj end
		return getpath(obj[key], ...)
	end
	util.getpath = getpath
end

-- Structure builder, complement to optional chaining. Creates a chain
-- of tables as necessary at each key and puts the specified value at
-- the last key.

-- setpath(obj, a, b, c, v):
-- - if obj.a == nil then obj.a = {} end
-- - if obj.a.b == nil then obj.a.b = {} end
-- - obj.a.b = v
-- Deleting keys the last key is possible if v is explicitly nil, but
-- all other intervening table structure is still created.
do
	local function setpath(obj, key, firstarg, ...)
		if select("#", ...) == 0 then
			obj[key] = firstarg
			return firstarg
		end
		local t = obj[key]
		if t == nil then
			t = {}
			obj[key] = t
		end
		return setpath(t, firstarg, ...)
	end
	util.setpath = setpath
end

------------------------------------------------------------------------
-- Table Invert

-- Swaps keys and values, assuming table is 1:1.
-- If it isn't, an arbitrary choice is made from duplicates and the
-- resulting table has fewer keys than the original.
function util.table_invert(t)
	local u = {}
	for k, v in pairs(t) do u[v] = k end
	return u
end

------------------------------------------------------------------------
-- Deep Copy

-- Should support aliases and cycles as well
local function deepcopy_core(src, map)
	if type(src) ~= "table" then return src end
	local found = map[src]
	if found then return found end
	local copy = {}
	map[src] = copy
	for k, v in pairs(src) do
		copy[deepcopy_core(k, map)] = deepcopy_core(v, map)
	end
	return copy
end
function util.deepcopy(src) return deepcopy_core(src, {}) end

------------------------------------------------------------------------
-- Event Emitter

-- Create the paired core functions of an event emitter
-- Return the individual functions like a mixin
function util.create_emitter()
	local callbacks = {}
	local function on(event, func)
		local cbs = callbacks[event]
		if not cbs then
			cbs = {}
			callbacks[event] = cbs
		end
		cbs[#cbs + 1] = func
	end
	-- returns truthy if there was a handler, else falsy
	local function emit(event, ...)
		local cbs = callbacks[event]
		if not cbs then return end
		for i = 1, #cbs do
			cbs[i](...)
		end
		if event then emit(false, event, ...) end
		return #cbs > 0 or nil
	end
	return on, emit
end

------------------------------------------------------------------------
-- Logging

-- Standardized logging with structured data support
function util.log(lv, ...)
	local t = {...}
	for i = 1, #t do
		if type(t[i]) == "table" then
			t[i] = string_gsub(dump(t[i]), "\n", " ")
		end
	end
	return core.log(lv, string_gsub(table_concat(t, " "), "%s+", " "))
end

------------------------------------------------------------------------
-- Structured Mod Storage Wrapper

-- local get = util.structstore(modstore[, prefix])
-- local data, save = get(key)
-- save()
function util.structstore(modstore, prefix)
	local cache = {}
	return function(key)
		local fullkey = (prefix or (modname .. "_"
				.. core.get_game_info().id)) .. "_" .. key
		local found = cache[fullkey]
		if found then return unpack(found) end
		local s = modstore:get_string(fullkey)
		local data = s and core.deserialize(s) or {}
		local function save()
			modstore:set_string(fullkey, core.serialize(data))
		end
		cache[fullkey] = {data, save}
		return data, save
	end
end

------------------------------------------------------------------------
-- Generate UUID

-- UUID v4 format using the naive RNG
function util.generate_uuid()
	return string_gsub(string_gsub("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx",
			"x", function() return string_format("%x", math_random(0, 15)) end),
		"y", function() return string_format("%x", math_random(0, 15)) end)
end

------------------------------------------------------------------------
-- Name lookup helpers

local function get_slot_game(state, slot_id)
	return util.getpath(state, "slot_info", slot_id, "game")
end
util.get_slot_game = get_slot_game

local function get_location_name(state, game, location_id)
	return util.getpath(state, "data_package", "games", game,
		"location_id_to_name", location_id)
end
util.get_location_name = get_location_name

local function get_item_name(state, game, item_id)
	return util.getpath(state, "data_package", "games", game,
		"item_id_to_name", item_id)
end
util.get_item_name = get_item_name

local function get_player_name(state, player_id)
	local found = util.getpath(state, "players_index", player_id)
	return found and (found.alias or found.name)
end
util.get_player_name = get_player_name

------------------------------------------------------------------------
-- Convert PrintJSON to Text

-- Define lookups by type
local printjson_lookups = {
	item_id = function(state, item)
		return get_item_name(state, get_slot_game(state, item.player), tonumber(item.text))
	end,
	location_id = function(state, item)
		return get_location_name(state, get_slot_game(state, item.player), tonumber(item.text))
	end,
	player_id = function(state, item)
		return get_player_name(state, tonumber(item.text))
	end
}

-- Convert text using given connection state from
-- PrintJSON command payload data
function util.print_json_convert(state, data)
	local parts = {}
	for i = 1, #data do
		local item = data[i]
		local text = item.text or ""
		local lookup = item.type and printjson_lookups[item.type]
		if lookup then text = lookup(state, item) end
		parts[i] = text
	end
	return table_concat(parts)
end

-----------------------------------------------------------------------

return util
