-- Library for serializing items in Factorio
-- Based on code from playerManager and trainTeleports
local compat = require("modules/clusterio/compat")
local serialize = {}

-- 0.17 compatibility
local supports_bar, get_bar, set_bar
if compat.version_ge("1.0.0") then
	supports_bar = "supports_bar"
	get_bar = "get_bar"
	set_bar = "set_bar"
else
	supports_bar = "hasbar"
	get_bar = "getbar"
	set_bar = "setbar"
end

local has_create_grid = compat.version_ge("1.1.7")
local has_quality = compat.version_ge("2.0.0")

-- Equipment Grids are serialized into an array of equipment entries
-- where ench entry is a table with the following fields:
--	 n: name
--	 g: ghost name (optional)
--	 t: to_be_removed (optional)
--	 p: position (array of 2 numbers corresponding to x and y)
--	 s: shield (optional)
--	 e: energy (optional)
-- If the equipment is a burner the following is also present:
--	 i: burner inventory
--	 r: result inventory
--	 b: currently burning (optional)
--	 f: remaining_burning_fuel (optional)
-- If the equipment has a quality the following is also present:
--	 q: quality (optional)
function serialize.serialize_equipment_grid(grid)
	local serialized = {}
	local processed = {}
	for y = 0, grid.height - 1 do
		for x = 0, grid.width - 1 do
			local equipment = grid.get({x, y})
			if equipment ~= nil then
				local pos = equipment.position
				local combined_pos = pos.x + pos.y * grid.width + 1
				if not processed[combined_pos] then
					processed[combined_pos] = true
					local entry = {
						n = equipment.name,
						p = {pos.x, pos.y},
					}
					if equipment.type == "equipment-ghost" then
						if equipment.ghost_name then
							entry.g = equipment.ghost_name
						end
					end
					if equipment.shield > 0 then entry.s = equipment.shield end
					if equipment.energy > 0 then entry.e = equipment.energy end
					if equipment.burner then
						local burner = equipment.burner
						entry.i = serialize.serialize_inventory(burner.inventory)
						entry.r = serialize.serialize_inventory(burner.burnt_result_inventory)
						if burner.currently_burning then
							entry.b = burner.currently_burning.name
							entry.f = burner.remaining_burning_fuel
						end
					end
					if equipment.quality then
						entry.q = equipment.quality.name
					end
					if equipment.to_be_removed then
						entry.t = equipment.to_be_removed
					end
					table.insert(serialized, entry)
				end
			end
		end
	end
	return serialized
end

function serialize.deserialize_equipment_grid(grid, serialized)
	grid.clear()
	for _, entry in ipairs(serialized) do
		local equipment = grid.put({
			name = entry.g or entry.n,
			quality = has_quality and entry.q or nil,
			position = entry.p,
			ghost = entry.g and true or nil,
		})
		if equipment then
			if entry.s then equipment.shield = entry.s end
			if entry.e then equipment.energy = entry.e end
			if entry.i then
				local burner = equipment.burner
				if entry.b then burner.currently_burning = entry.b end
				if entry.f then burner.remaining_burning_fuel = entry.f end
				serialize.deserialize_inventory(burner.burnt_result_inventory, entry.r)
				serialize.deserialize_inventory(burner.inventory, entry.i)
			end
		end
	end
end

-- Item stacks are serialized into a table with the following fields:
--	 n: name
--	 c: count
--	 q: quality (optional)
--	 h: health (optional)
--	 d: durability (optional)
--	 a: ammo count (optional)
--	 l: label (optional)
--	 g: equipment grid (optional)
--	 i: item inventory (optional)
-- If the item stack is exportable it has the following property instead
--	 e: export string
-- Label is a table with the following fields:
--	 t: label text (optional)
--	 c: color (optional)
--	 a: allow manual label change
function serialize.serialize_item_stack(slot, entry)
	if
		slot.is_blueprint
		or slot.is_blueprint_book
		or slot.is_upgrade_item
		or slot.is_deconstruction_item
		or slot.is_item_with_tags
	then
		local call_success, call_return = pcall(slot.export_stack)
		if not call_success then
			print("Error: '" .. call_return .. "' thrown exporting '" .. slot.name .. "'")
		else
			entry.e = call_return
		end

		return
	end

	entry.n = slot.name
	entry.c = slot.count
	if has_quality and slot.quality.level ~= 0 then entry.q = slot.quality.name end
	if slot.health < 1 then entry.h = slot.health end
	if slot.type == "tool" and slot.durability then entry.d = slot.durability end
	if slot.type == "ammo" then entry.a = slot.ammo end
	if slot.is_item_with_label then
		local label = {}
		if slot.label then label.t = slot.label end
		if slot.label_color then label.c = slot.label_color end
		label.a = slot.allow_manual_label_change
		entry.l = label
	end

	if slot.grid then
		entry.g = serialize.serialize_equipment_grid(slot.grid)
	end

	if slot.is_item_with_inventory then
		local sub_inventory = slot.get_inventory(defines.inventory.item_main)
		entry.i = serialize.serialize_inventory(sub_inventory)
	end
end

function serialize.deserialize_item_stack(slot, entry)
	if entry.e then
		local success = slot.import_stack(entry.e)
		if success == 1 then
			print("Error: import of '" .. entry.e .. "' succeeded with errors")
		elseif success == -1 then
			print("Error: import of '" .. entry.e .. "' failed")
		end

		return
	end

	local item_stack = {
		name = entry.n,
		count = entry.c,
	}
	if entry.q then item_stack.quality = entry.q end
	if entry.h then item_stack.health = entry.h end
	if entry.d then item_stack.durability = entry.d end
	if entry.a then item_stack.ammo = entry.a end

	local call_success, call_return = pcall(slot.set_stack, item_stack)
	if not call_success then
		print("Error: '" .. call_return .. "' thrown setting stack ".. serpent.line(entry))

	elseif not call_return then
		print("Error: Failed to set stack " .. serpent.line(entry))

	else
		if entry.l then
			-- TODO test this with AAI's unit-remote-control
			local label = entry.l
			if label.t then slot.label = label.t end
			if label.c then slot.label_color = label.c end
			slot.allow_manual_label_change = label.a
		end
		if entry.g then
			if slot.grid then
				serialize.deserialize_equipment_grid(slot.grid, entry.g)
			elseif slot.type == "item-with-entity-data" and has_create_grid then
				slot.create_grid()
				serialize.deserialize_equipment_grid(slot.grid, entry.g)
			else
				print("Error: Attempt to deserialize equipment grid on an unsupported entity")
			end
		end
		if entry.i then
			local sub_inventory = slot.get_inventory(defines.inventory.item_main)
			serialize.deserialize_inventory(sub_inventory, entry.i)
		end
	end
end

-- Inventories are serialized into a table with the following fields:
--	 i: array of item stack or exportable item entries
--	 b: bar position (optional)
-- Each item entry has the following fields
--	 s: index (optional, equals to previous plus one if not present)
--	 r: repeat count (optional)
--	 f: slot filter (optional)
-- Pluss all the fields for item stacks (see deserialize_item_stack)
-- It's also possible that the slot is empty but has a slot filter.
function serialize.serialize_inventory(inventory)
	local serialized = {}
	if inventory[supports_bar]() and inventory[get_bar]() <= #inventory then
		serialized.b = inventory[get_bar]()
	end

	serialized.i = {}
	local previous_index = 0
	local previous_serialized = nil
	for i = 1, #inventory do
		local item = {}
		local slot = inventory[i]
		if inventory.supports_filters() then
			item.f = inventory.get_filter(i)
		end

		if slot.valid_for_read then
			serialize.serialize_item_stack(slot, item)
		end

		if item.n or item.f or item.e then
			local item_serialized = compat.table_to_json(item)
			if item_serialized == previous_serialized then
				local previous_item = serialized.i[#serialized.i]
				previous_item.r = (previous_item.r or 0) + 1
				previous_index = i

			else
				if i ~= previous_index + 1 then
					item.s = i
				end

				previous_index = i
				previous_serialized = item_serialized
				table.insert(serialized.i, item)
			end

		else
			-- Either an empty slot or serilization failed
			previous_index = 0
			previous_serialized = nil
		end
	end

	return serialized
end

function serialize.deserialize_inventory(inventory, serialized)
	if serialized.b and inventory[supports_bar]() then
		inventory[set_bar](serialized.b)
	end

	local last_slot_index = 0
	for _, entry in ipairs(serialized.i) do
		local base_index = entry.s or last_slot_index + 1

		local repeat_count = entry.r or 0
		for offset = 0, repeat_count do
			local index = base_index + offset
			if index > #inventory then
				print("Error: Inventory index out of bounds deserializing inventory, voiding excess slots")
				break
			end
			local slot = inventory[index]
			if entry.f then
				local call_success, call_return = pcall(inventory.set_filter, index, entry.f)
				if not call_success then
					print("Error: '" .. call_return .. "' thrown setting filter " .. entry.f)

				elseif not call_return then
					print("Error: Failed to set filter " .. entry.f)
				end
			end

			if entry.n or entry.e then
				serialize.deserialize_item_stack(slot, entry)
			end
		end
		last_slot_index = base_index + repeat_count
	end
end

return serialize
