--[[	Zone Recycle Bin
-- ======================================================================
** 	Author: Catspaw (CatspawMods @ ModDB)
** 	Source: https://www.moddb.com/members/catspawmods/addons
** 	Version: 1.4
**	Updated: 20230625
	You may modify any part of this mod and do whatever you like with it, 
	just give credit where due. 
	
	Most of this file unavoidably duplicates the majority of Alundaio's 
	vanilla script so that the Recycle Bin will behave just like any other 
	placeable stash interaction.

	ANY AND ALL ITEMS PLACED INSIDE THE CONTAINER WILL BE PERMANENTLY 
	DESTROYED AFTER A SHORT DELAY.

	Credits: 
		* Alundaio and Tronex for the vanilla backpack script
		* TB's New Stash Locations for the bin's in-game model (from GSC)
		* PYP for the smoothing fix to GSC's vanilla model
		* Asametar for RUS and UKR localization
		* artifax for the workshop autoinject script
		* demonized for the time events

	If another addon also has a copy of the same workshop or time events 
	scripts, it should not matter whose you use.
-- ======================================================================
	To uninstall without MCM, first:
		1. Pick up any placed Zone Recycle Bins and save your game
		2. Quit out and set the following line to true:				 --]]
local uninstall 				= false
--		3. Then re-launch the game, load, and make a new separate save
-- 		4. It is then safe to uninstall this addon--the new save is clean
local trashcan_deletion_timeout = 30
-- After this many seconds in the trashcan, an item is gone forever!
-- Removing an item from the container before the timeout will prevent 
-- its destruction, but I don't recommend playing chicken with this fact.
-- Configurable through MCM, if you have it.
-- ======================================================================
local started 				= false
local debuglogs 			= false		-- Debug logging enabled
local verbose 				= true		-- Verbose logging (noisy!)
local killswitch 			= false 	-- Disables all callbacks (for debugging/testing)
local deletion_enabled 		= true  	-- If false, nothing is actually deleted but everything works (for debugging)
local show_mapspots 		= false		-- If true, show a special map icon for the Recycle Bin
local mollyguard 			= true 		-- If true, the player will get warned with shouty all-caps messages about how deleting stuff works
local add_new_recipe 		= true 		-- If true, the mod will add a crafting recipe for the Recycle Bin
local recipe_added			= false 	-- Flag to prevent dupe recipes from being added
local gimme_one 			= false
-- If true, the script will spawn a copy of the bin in the player's 
-- inventory. It will do this every single time the game loads if you 
-- hardcode it here, so I recommend using MCM instead.

-- ======================================================================
-- Messing with these string constants will only bring you sadness
-- ======================================================================
local mapspot_string 		= "trash"	
local bin_placeable 		= "itm_placeable_trashcan"
local bin_worlditem 		= "inv_trashcan"
local queue_name 			= "delete_trash_"
local CreateTimeEvent 		= demonized_time_events.CreateTimeEvent
local RemoveTimeEvent 		= demonized_time_events.RemoveTimeEvent

-- ======================================================================
-- Definitions for the optional crafting recipe.
-- Valid IDs for parts are found in configs\items\settings\parts.ltx
-- ======================================================================
local req_special_mat 		= false
-- If you want to make the bin require rare materials to craft
local special_mat 			= "af_death_lamp" -- can be any valid item
local bin_craft_string 		= {
	["itm_placeable_trashcan"] = {
			index 	= 2, -- Workshop tab
			tier 	= "1", -- basic tools
			recipe 	= "recipe_basic_0", -- no recipe requirement
			mats	= {
				[1] = {mat="prt_i_scrap",ct="10"},		-- def: 10 metal scrap
				[2] = {mat="prt_i_fasteners",ct="4"},	-- def: 4 fasteners
			-- Up to four ingredients allowed
			-- fourth will be overridden if req_special_mat is enabled
			}
		}
	}
local last_interaction = time_global()
local garbage_interval = 360
trash_by_trash_id = {}
trash_by_story_id = {}

-- ======================================================================

local scriptname = "<ZRB>"
function dl(logtext,...)
	-- Debug logging
	local prefix = scriptname..": "
	if logtext and debuglogs then 
		--if verbose then
			-- local cf = debug.getinfo(2, 'n').name or ""
			--prefix = scriptname.."."..cf.."[V]: "
		--end
		printf(prefix..logtext,...)
	end
end

function vl(logtext,...)
	-- Verbose logging
	if verbose and debuglogs then
		dl(logtext,...)
	end
end

function check_new_recipe(enabled,item,customstring)
	if enabled and item then
		if not recipe_added then
			local recipe_string = customstring or "1,recipe_basic_0,prt_i_scrap,1"
			local bcs = bin_craft_string[item] or nil
			if bcs and not customstring then
				local rs = ""
				if req_special_mat then 
					bcs.mats[4] = {mat=special_mat,ct="1"}
				end
				for k,v in pairs(bcs.mats) do
					if k then
						local bm = bcs.mats[k]
						local mat = bm.mat or ""
						local ct = bm.ct or ""
						rs = rs..","..mat..","..ct
					end
				end
				recipe_string = bcs.tier..","..bcs.recipe..rs
				dl("Trying to inject new recipe for %s in workshop tab %s: %s",item,index,recipe_string)
				workshop_autoinject.add_new_recipe(bcs.index,item,recipe_string)
			end
			recipe_added = true
		end
	end
end

function clear_trash_record(id_or_tid)
	vl("clear_trash_record(%s) called",id_or_tid)
	if not id_or_tid then return end
	local trash,trash_id,id
	if type(id_or_tid) == "number" then
		trash = trash_by_story_id
		id = id_or_tid
		trash_id = trash and trash[id] and trash[id].trash_id
	elseif type(id_or_tid) == "string" then
		trash = trash_by_trash_id
		trash_id = id_or_tid
		id = trash and trash[trash_id] and trash[trash_id].id
	else return	end
	if id and trash_by_story_id[id] then
		vl("clearing record for trash_by_story_id[%s]",id)
		trash_by_story_id[id] = nil
	end
	if trash_id and trash_by_trash_id[trash_id] then
		vl("clearing record for trash_by_trash_id[%s]",trash_id)
		trash_by_trash_id[trash_id] = nil
	end
	local obj = get_object_by_id(id)
	local name = obj and obj:name() or "unknown"
	dl("%s (%s) removed from %s, removing %s from deletion queue",name,id,bin_worlditem,trash_id)
	return true
end

function delete_item(obj,trash_id,uninstall)
	local id
	if not obj then
		id = trash_by_trash_id[trash_id] and trash_by_trash_id[trash_id].id
		if id then
			obj = get_object_by_id(id)
			if not obj then
				dl("WARNING: no valid object or trash_id passed to delete_item!")
				return
			end
		end
	else
		id = obj:id()
	end
	if deletion_enabled or uninstall then 
		if uninstall then
			dl("Uninstall: deleting copy of %s",bin_placeable)
		else
			dl("%s timed out, deleting id %s from recycle bin",obj:name(),id)
		end
		local se_obj = alife_object(id)
		local success = alife_release_id(id)
		if success then
			dl("trash_id %s | obj id %s released successfully",id,trash_id)
			clear_trash_record(id)
		else
			dl("WARNING: object id %s could not be released for some reason!",id)
		end
	else
		dl("deletion_enabled was set to %s, nothing done with id %s",deletion_enabled,id)
		clear_trash_record(id)
	end
	return true
end

function process_queued_item(obj,trash_id)
	if trash_id and trash_by_trash_id and trash_by_trash_id[trash_id] then
		vl("queued item %s still has trash record %s, passing to delete_item",obj and obj:id(),trash_id)
		delete_item(obj,trash_id)
	end
	return true
end

function queue_item_deletion(obj)
	if obj and deletion_enabled then
		local tg = time_global()
		local id = obj:id()
		local trash_id = tostring(tg).."_"..tostring(id).."_"..tostring(math.random(1,1000))
		trash_by_trash_id[trash_id] = {id = id,added = tg}
		trash_by_story_id[id] = {}
		trash_by_story_id[id].trash_id = trash_id
		dl("Actor put item %s in trash can, queueing for deletion at %s with trash_id %s",obj:section(),tg,trash_id)
		CreateTimeEvent("trashcan_item_deletion",queue_name..trash_id,trashcan_deletion_timeout,process_queued_item,obj,trash_id)
	else
		dl("deletion_enabled is %s, %s not queued for deletion",deletion_enabled,obj:section())
	end
end

function get_trash_record(id_or_tid,tbl)
	if not id_or_tid then return end
	local trash = tbl or nil
	if type(id_or_tid) == "number" then
		trash = trash or trash_by_story_id
	elseif type(id_or_tid) == "string" then
		trash = trash or trash_by_trash_id
	else return	end

	return id_or_tid and trash and trash[id_or_tid]
end

function garbage_collection()
	if trash_by_trash_id and not is_empty(trash_by_trash_id) then
		for trash_id,td in pairs(trash_by_trash_id) do
			local id = td and td.id
			local obj
			if id then obj = get_object_by_id(id) end
			if obj then
				queue_item_deletion(obj,trash_id)
				dl("TID %s: unprocessed trash for id %s, queueing for deletion")
			else
				trash_by_trash_id[trash_id] = nil
				dl("TID %s: no object for id %s, clearing trash record")				
			end
		end
		CreateTimeEvent("zrb_garbage_collection",0,garbage_interval,garbage_collection)
	end
	return true
end

function uninstall_cleanup()
	local function search(temp , item)
		if item:section() == bin_placeable then
			delete_item(item,nil,true)
		end
	end
	if started then
		db.actor:iterate_inventory(search,nil)
	end
	uninstall = false
	return true
end

function menu_stash(obj) -- return context menu "use" name
	local p = obj:parent()
	if not (p and p:id() == AC_ID) then return end
	vl("player right-clicked on %s",obj:section())
	if obj and (obj:section() == bin_placeable) then
		return game.translate_string("st_place_trashcan")
	end
end

function func_stash(obj)
	start(obj)
end

function on_option_change()
	if ui_mcm then
		-- If you have MCM, if will override the hardcoded settings
		deletion_enabled 			= ui_mcm.get("zrb/mod_enabled")
		trashcan_deletion_timeout 	= ui_mcm.get("zrb/deletion_timeout")
		show_mapspots 	 			= ui_mcm.get("zrb/use_mapspots")
		mollyguard					= ui_mcm.get("zrb/warn_enabled")
		add_new_recipe				= ui_mcm.get("zrb/add_recipe")
		gimme_one 					= ui_mcm.get("zrb/spawn_bin")
		uninstall 					= ui_mcm.get("zrb/uninstall")
		dl("MCM options synced, enabled=%s. | timeout %s | mapspots: %s | warn: %s | gimme: %s",deletion_enabled,trashcan_deletion_timeout,show_mapspots,mollyguard,gimme_one)
	end
	if gimme_one then
		dl("Giving a free Recycle Bin to the player")
		alife_create_item(bin_placeable, db.actor)
		gimme_one = false
		ui_mcm.set("zrb/spawn_bin",gimme_one)
	end
	if uninstall then
		dl("Removing all copies of the Recycle Bin from player's inventory")
		uninstall_cleanup()
		ui_mcm.set("zrb/uninstall",false)
	end
	check_new_recipe(add_new_recipe,bin_placeable)
	Unregister_UI("UICreateTrash")
end

function actor_on_item_put_in_box(box,obj)
	if (box:section() == bin_worlditem) then
		last_interaction = time_global()
		queue_item_deletion(obj)
	end
end

function actor_on_item_take_from_box(box,obj)
	if box:section() ~= bin_worlditem then return end
	local id = obj:id()
	local name = obj:name()
	dl("Player took %s (%s) from %s, processing",name,id,bin_worlditem)
	last_interaction = time_global()
	if (box:is_inv_box_empty()) then
		hide_hud_inventory()
		local box_id = box:id()
		local data = {
			stash_id = box_id,
			cancel = false,
		}
		SendScriptCallback("actor_on_stash_remove",data)
		if data.cancel then
			dl("stash removal for %s canceled by another script",box_id)
		else
			if show_mapspots then
				level.map_remove_object_spot(box_id, mapspot_string)
			end
			local se_obj = alife_object(box_id)
			if se_obj then
				alife_release(se_obj)
			end
			
			local m_data = alife_storage_manager.get_state()
			if (m_data.player_created_stashes and m_data.player_created_stashes[box_id]) then 
				local section = m_data.player_created_stashes[box_id]
				alife_create_item(section, db.actor)
				m_data.player_created_stashes[box_id] = nil
			end
		end
	end

	if trash_by_story_id[id] then
		local trash_id = trash_by_story_id[id].trash_id
		if trash_id then RemoveTimeEvent("trashcan_item_deletion",queue_name..trash_id) end
		clear_trash_record(id)
	end
end

function actor_on_item_use(obj)
	func_stash()
end

function actor_on_first_update()
	on_option_change()
	started = true
	garbage_collection()
end


function load_state(data)
	if not data and data.zrb_data then return end
	local zrb_data = data and data.zrb_data
	trash_by_trash_id = (zrb_data and zrb_data.trash_by_trash_id) or {}
end


function save_state(data)
	if trash_by_trash_id and not is_empty(trash_by_trash_id) then
		local zrb_data = {trash_by_trash_id = trash_by_trash_id}
		data.zrb_data = zrb_data
	end
end

function on_game_start()
	if not killswitch then
		RegisterScriptCallback("actor_on_item_take_from_box",actor_on_item_take_from_box)
		RegisterScriptCallback("actor_on_item_put_in_box",actor_on_item_put_in_box)
		RegisterScriptCallback("actor_on_item_use",actor_on_item_use)
		RegisterScriptCallback("actor_on_first_update",actor_on_first_update)
		RegisterScriptCallback("on_option_change",on_option_change)
		RegisterScriptCallback("save_state",save_state)
		RegisterScriptCallback("load_state",load_state)
	end
end


--[[=======================================================================
	BACKPACK CODE
	Everything that follows is copied directly from the vanilla 
	item_backpack.script file. For the most part, the only real changes 
	have been strings and IDs, like changing the section "inv_backpack" 
	to "inv_trashcan", etc.

	Could probably get rid of nearly all of this if we didn't care 
	about the player being able to name the bin's mapspot.
--=====================================================================--]]

GUI = nil -- instance, don't touch
function start(obj)
	if (not obj) then
		return
	end
	
	hide_hud_inventory()
	
	if (not GUI) then
		GUI = UICreateTrash()
	end
	
	if (GUI) and (not GUI:IsShown()) then
		GUI:ShowDialog(true)
		GUI:Reset(obj)
		
		Register_UI("UICreateTrash","item_trashcan")
	end
end

class "UICreateTrash" (CUIScriptWnd)

function UICreateTrash:__init() super()
	self:InitControls()
	self:InitCallBacks()
end

function UICreateTrash:__finalize()
end

function UICreateTrash:InitControls()
	self:SetWndRect(Frect():set(0,0,1024,768))
	self:SetAutoDelete(true)
	--self:Enable(true)

	local xml					= CScriptXmlInit()
	xml:ParseFile				("ui_items_backpack.xml")

	self.dialog					= xml:InitStatic("backpack", self)
	xml:InitStatic("backpack:background", self.dialog)

	self.input = xml:InitEditBox("backpack:input",self.dialog)
	self:Register(self.input,"fld_input")

	local btn = xml:Init3tButton("backpack:btn_cancel", self.dialog)
	self:Register(btn,"btn_cancel")

	btn = xml:Init3tButton("backpack:btn_ok", self.dialog)
	self:Register(btn,"btn_ok")
end

function UICreateTrash:InitCallBacks()
	self:AddCallback("btn_ok", ui_events.BUTTON_CLICKED, self.OnAccept, self)
	self:AddCallback("btn_cancel", ui_events.BUTTON_CLICKED, self.Close, self)
end

function UICreateTrash:Reset(obj)
	self.id = obj:id()
	self.section = obj:section()
	
	self.input:SetText("")
end

function UICreateTrash:Update()
	CUIScriptWnd.Update(self)
end

function UICreateTrash:OnAccept()
	local se_obj = alife_create(bin_worlditem,db.actor:position(),db.actor:level_vertex_id(),db.actor:game_vertex_id())
	if (se_obj) then
		local txt = self.input:GetText()
		txt = txt ~= "" and txt or strformat("%s's Recycle Bin",db.actor:character_name())
		if show_mapspots then
			level.map_add_object_spot(se_obj.id, mapspot_string, txt)
		end
		local showtxt = "Recycle Bin placed"
		if mollyguard then 
			showtxt = game.translate_string("st_trashcan_mollyguard")..
			" "..tostring(trashcan_deletion_timeout).." "..
			game.translate_string("st_trashcan_seconds").."!"
		else
			showtxt = game.translate_string("st_trashcan_shutupwesley")
		end
		dl("mollyguard=%s | "..showtxt,mollyguard)
		actor_menu.set_msg(1,showtxt,4)
		local m_data = alife_storage_manager.get_state()
		if not (m_data.player_created_stashes) then 
			m_data.player_created_stashes = {}
		end
		m_data.player_created_stashes[se_obj.id] = self.section
		alife_release_id(self.id)
		local data = {
			stash_id = se_obj.id,
			stash_name = txt,
			stash_section = self.section,
		}

		SendScriptCallback("actor_on_stash_create",data)
	end
	self:Close()
end

function UICreateTrash:OnKeyboard(dik, keyboard_action)
	local res = CUIScriptWnd.OnKeyboard(self,dik,keyboard_action)
	if (res == false) then
		local bind = dik_to_bind(dik)
		if keyboard_action == ui_events.WINDOW_KEY_PRESSED then
			if dik == DIK_keys.DIK_ESCAPE then
				self:Close()
			end
		end
	end
	return res
end

function UICreateTrash:Close()
	self:HideDialog()
	
	Unregister_UI("UICreateTrash")
end