﻿-- chunkname: @scripts/extension_systems/buff/buff_extension_base.lua

local BuffClasses = require("scripts/settings/buff/buff_classes")
local BuffExtensionInterface = require("scripts/extension_systems/buff/buff_extension_interface")
local BuffSettings = require("scripts/settings/buff/buff_settings")
local BuffTemplates = require("scripts/settings/buff/buff_templates")
local FixedFrame = require("scripts/utilities/fixed_frame")
local PortableRandom = require("scripts/foundation/utilities/portable_random")
local WeaponTemplate = require("scripts/utilities/weapon/weapon_template")
local BUFF_TARGETS = BuffSettings.targets
local MIN_PROC_EVENTS_SIZE = BuffSettings.min_proc_events_size
local MAX_PROC_EVENTS = BuffSettings.max_proc_events
local PROC_EVENTS_STRIDE = BuffSettings.proc_events_stride
local minion_effects_priorities = BuffSettings.minion_effects_priorities
local BuffExtensionBase = class("BuffExtensionBase")
local Unit_world_position = Unit.world_position
local Unit_node = Unit.node
local Unit_has_node = Unit.has_node
local WwiseWorld_set_source_position = WwiseWorld.set_source_position
local RPCS = {
	"rpc_add_buff",
	"rpc_remove_buff",
	"rpc_buff_proc_set_active_time",
	"rpc_buff_set_start_time",
	"rpc_add_buff_with_stacks",
}
local _stat_buff_base_values = BuffSettings.stat_buff_type_base_values
local _stat_buff_lazy_mt = {
	__index = function (self, key)
		local val = _stat_buff_base_values[key]

		self[key] = val

		return val
	end,
}

BuffExtensionBase.init = function (self, extension_init_context, unit, extension_init_data, game_object_data_or_game_session, nil_or_game_object_id, pre_allocate_event_param_tables)
	local is_server = extension_init_context.is_server

	self._is_server = is_server
	self._unit = unit
	self._player = extension_init_data.player

	local seed = extension_init_data.buff_seed

	self._portable_random = PortableRandom:new(seed)
	self._local_portable_random = PortableRandom:new(seed)
	self._buff_system = Managers.state.extension:system("buff_system")
	self._buff_context = {
		world = extension_init_context.world,
		physics_world = extension_init_context.physics_world,
		wwise_world = extension_init_context.wwise_world,
		unit = unit,
		player = extension_init_data.player,
		buff_extension = self,
		is_local_unit = extension_init_data.is_local_unit,
		is_server = is_server,
		breed = extension_init_data.breed,
		fixed_time_step = Managers.state.game_session.fixed_time_step,
	}
	self._index = 0
	self._buffs = {}
	self._muted_external_buffs = {}
	self._buff_instance_id = 0
	self._stacking_buffs = {}
	self._buffs_by_index = {}
	self._stat_buffs = setmetatable({
		_modified_stats = {},
	}, _stat_buff_lazy_mt)
	self._keywords = {}
	self._had_keywords = {}
	self._sfx_node_effects = {}
	self._vfx_node_effects = {}
	self._proc_event_param_tables = {}
	self._num_proc_events = 0
	self._param_tables_start_index_reference = 1
	self._num_params_table_in_use = 0
	self._unique_frame_proc = {}

	if pre_allocate_event_param_tables then
		self._proc_events = Script.new_array(MAX_PROC_EVENTS * PROC_EVENTS_STRIDE)
		self._proc_events_backup = Script.new_array(MAX_PROC_EVENTS * PROC_EVENTS_STRIDE)

		for i = 1, MAX_PROC_EVENTS do
			self._proc_event_param_tables[i] = {}
		end
	else
		self._proc_events = Script.new_array(MIN_PROC_EVENTS_SIZE * PROC_EVENTS_STRIDE)
		self._proc_events_backup = Script.new_array(MIN_PROC_EVENTS_SIZE * PROC_EVENTS_STRIDE)
	end

	if is_server then
		self._buffs_added_before_game_object_creation = {}

		local initial_buffs = extension_init_data.initial_buffs

		if initial_buffs then
			local t = FixedFrame.get_latest_fixed_time()

			for i = 1, #initial_buffs do
				local buff_name = initial_buffs[i]

				self:add_internally_controlled_buff(buff_name, t)
			end
		end
	else
		local network_event_delegate = extension_init_context.network_event_delegate

		self._network_event_delegate = network_event_delegate
		self._game_object_id = nil_or_game_object_id

		network_event_delegate:register_session_unit_events(self, nil_or_game_object_id, unpack(RPCS))

		self._rpcs_registered = true
		self._buff_index_map = {}
	end

	self._fixed_time_step = Managers.state.game_session.fixed_time_step
end

BuffExtensionBase.destroy = function (self)
	local buffs = self._buffs_by_index
	local extension_destroyed = true

	for index, _ in pairs(buffs) do
		self:_remove_buff(index, extension_destroyed)
	end

	local is_server = self._is_server

	if not is_server and self._rpcs_registered then
		self._network_event_delegate:unregister_unit_events(self._game_object_id, unpack(RPCS))

		self._rpcs_registered = false
	end
end

BuffExtensionBase.game_object_initialized = function (self, game_session, game_object_id)
	ferror("Buff extension is using base implementation of game_object_initialized, it shouldn't")
end

BuffExtensionBase.set_unit_local = function (self)
	local buff_context = self._buff_context

	buff_context.is_local_unit = true

	local buffs = self._buffs_by_index

	for index, buff_instance in pairs(buffs) do
		self:_remove_buff(index)
	end

	local is_server = self._is_server

	if not is_server and self._rpcs_registered then
		self._network_event_delegate:unregister_unit_events(self._game_object_id, unpack(RPCS))

		self._rpcs_registered = false
	end
end

BuffExtensionBase.hot_join_sync = function (self, unit, sender, channel)
	ferror("Buff extension is using base implementation of hot_join_sync, it shouldn't")
end

BuffExtensionBase._update_buffs = function (self, dt, t)
	local buffs = self._buffs
	local portable_random = self._portable_random

	for i = 1, #buffs do
		buffs[i]:update(dt, t, portable_random)
	end

	for index, buff_instance in pairs(self._buffs_by_index) do
		local finished = buff_instance:finished()
		local should_remove_stack, last_stack = buff_instance:should_remove_stack()

		if finished or should_remove_stack then
			self:_remove_internally_controlled_buff(index)
		end

		if not last_stack and should_remove_stack then
			buff_instance:removed_stack_by_request()
		end
	end
end

BuffExtensionBase.progressbar = function (self)
	local buffs = self._buffs

	for i = 1, #buffs do
		local buff = buffs[i]
		local progress_bar = buff:progressbar()

		if progress_bar then
			return progress_bar
		end
	end

	return nil
end

BuffExtensionBase._move_looping_sfx_sources = function (self, unit)
	local wwise_world = self._buff_context.wwise_world
	local sfx_node_effects = self._sfx_node_effects

	for node_index, entry in pairs(sfx_node_effects) do
		WwiseWorld_set_source_position(wwise_world, entry.wwise_source_id, Unit_world_position(unit, node_index))
	end
end

BuffExtensionBase._update_proc_events = function (self, t)
	local num_proc_events = self._num_proc_events

	if num_proc_events > 0 then
		local iterating_proc_events = self._proc_events
		local num_iterating_proc_events = num_proc_events

		self._proc_events = self._proc_events_backup
		self._num_proc_events = 0

		local buffs = self._buffs
		local portable_random = self._portable_random
		local local_portable_random = self._local_portable_random
		local is_server = self._is_server

		for i = 1, #buffs do
			local buff = buffs[i]
			local is_predicted = buff:is_predicted()
			local force_prediction = buff:force_predicted_proc()
			local skip_send_active_time_rpc = buff:skip_send_active_time_rpc()

			if buff and buff.update_proc_events and (is_server or is_predicted or force_prediction) then
				local activated_proc = buff:update_proc_events(t, iterating_proc_events, num_iterating_proc_events, portable_random, local_portable_random)

				if activated_proc then
					if is_server and not is_predicted then
						local server_index = self:_find_local_index(buff)

						self:_set_proc_active_start_time(server_index, t, skip_send_active_time_rpc)
					end

					if buff:remove_on_proc() then
						buff:force_finish()
					end
				end
			end
		end

		num_proc_events = self._num_proc_events

		table.clear(iterating_proc_events)

		self._proc_events_backup = iterating_proc_events

		self:_clear_param_tables(num_iterating_proc_events)

		self._num_params_table_in_use = num_proc_events

		if num_proc_events > 0 then
			-- Nothing
		end
	end

	table.clear(self._unique_frame_proc)
end

BuffExtensionBase._reset_stat_buffs = function (self)
	local stat_buff_base_values = BuffSettings.stat_buff_type_base_values
	local current_stat_buffs = self._stat_buffs
	local stats_to_reset = current_stat_buffs._modified_stats

	for key in pairs(stats_to_reset) do
		current_stat_buffs[key] = stat_buff_base_values[key]
	end

	table.clear(stats_to_reset)
end

BuffExtensionBase._update_stat_buffs_and_keywords = function (self, t, on_remove_buff)
	self:_reset_stat_buffs()

	local keywords = self._keywords

	if on_remove_buff then
		for keyword, _ in pairs(keywords) do
			self._had_keywords[keyword] = t
		end
	end

	table.clear(keywords)

	local buffs, current_stat_buffs = self._buffs, self._stat_buffs

	for i = 1, #buffs do
		local buff = buffs[i]

		if buff then
			buff:update_stat_buffs(current_stat_buffs, t)
			buff:update_keywords(keywords, t)
			buff:post_update_keywords_and_stats_func(t)
		end
	end
end

BuffExtensionBase._check_keywords = function (self, template)
	local forbidden_keywords = template.forbidden_keywords

	if forbidden_keywords then
		for i = 1, #forbidden_keywords do
			local keyword = forbidden_keywords[i]

			if self:has_keyword(keyword) then
				return false
			end
		end
	end

	local required_keywords = template.required_keywords

	if required_keywords then
		for i = 1, #required_keywords do
			local keyword = required_keywords[i]

			if not self:has_keyword(keyword) then
				return false
			end
		end
	end

	return true
end

BuffExtensionBase._can_add_internally_controlled_buff = function (self, template, t)
	local can_add_buff = self:_is_valid_target(template)

	if not can_add_buff then
		return false
	end

	can_add_buff = self:_handle_unique_buffs(template)

	if not can_add_buff then
		return false
	end

	can_add_buff = self:_check_max_stacks_cap(template, t)

	if not can_add_buff then
		return false
	end

	can_add_buff = self:_check_keywords(template)

	if not can_add_buff then
		return false
	end

	return true
end

BuffExtensionBase.add_internally_controlled_buff_with_stacks = function (self, template_name, number_of_stacks, t, ...)
	for i = 1, number_of_stacks do
		self:add_internally_controlled_buff(template_name, t, ...)
	end
end

BuffExtensionBase.add_internally_controlled_buff = function (self, template_name, t, ...)
	ferror("Buff extension is using base implementation of add_internally_controlled_buff, it shouldn't")
end

BuffExtensionBase.add_externally_controlled_buff = function (self, template_name, t, ...)
	ferror("Buff extension is using base implementation of add_externally_controlled_buff, it shouldn't")
end

BuffExtensionBase._next_local_index = function (self)
	local local_index = self._index + 1

	self._index = local_index

	return local_index
end

BuffExtensionBase._add_buff = function (self, template, t, ...)
	local local_index = self:_next_local_index()
	local template_name = template.name
	local can_stack = not not template.max_stacks
	local buff_instance

	if can_stack then
		local existing_buff_instance = self._stacking_buffs[template_name]

		if existing_buff_instance then
			local previous_stack_count = existing_buff_instance:stack_count()

			existing_buff_instance:add_stack()
			self:_on_add_buff_stack(existing_buff_instance, previous_stack_count)

			if template.refresh_duration_on_stack or template.refresh_start_time_on_stack then
				existing_buff_instance:set_start_time(t)
				existing_buff_instance:refresh_func(t, previous_stack_count)
				existing_buff_instance:set_need_to_sync_start_time(false)
			end

			self:_proc_on_buff_event(template_name, existing_buff_instance, BuffSettings.proc_events.on_buff_stack_added)

			buff_instance = existing_buff_instance
		end
	end

	if not buff_instance then
		local class_name = template.class_name
		local buff_class = BuffClasses[class_name]
		local buff_context = self._buff_context
		local buff_instance_id = self._buff_instance_id + 1

		buff_instance = buff_class:new(buff_context, template, t, buff_instance_id, ...)

		if not self._is_hub then
			self:_start_fx(buff_instance_id, template)
		end

		self:_on_add_buff(buff_instance)
		self:_proc_on_buff_event(template_name, buff_instance, BuffSettings.proc_events.on_buff_added)

		if can_stack then
			self._stacking_buffs[template_name] = buff_instance
		end

		self._buffs[#self._buffs + 1] = buff_instance
		self._buff_instance_id = buff_instance_id
	end

	self._buffs_by_index[local_index] = buff_instance

	if self._is_server then
		local template_context = buff_instance:template_context()
		local owner_unit = template_context.owner_unit
		local source_player = owner_unit and Managers.state.player_unit_spawn:owner(owner_unit)

		if source_player then
			local player_unit = source_player.player_unit
			local attacker_data_extension = ScriptUnit.has_extension(player_unit, "unit_data_system")
			local weapon_action_component = attacker_data_extension and attacker_data_extension:read_component("weapon_action")
			local target_weapon_template = weapon_action_component and WeaponTemplate.current_weapon_template(weapon_action_component)
			local weapon_template_name = target_weapon_template and target_weapon_template.name
			local breed_name = self._buff_context.breed.name
			local stack_count = template_context.stack_count

			Managers.stats:record_private("hook_buff", source_player, breed_name, template_name, stack_count, weapon_template_name)
		end
	end

	return local_index
end

BuffExtensionBase.is_valid_target = function (self, template_name)
	local template = BuffTemplates[template_name]

	return self:_is_valid_target(template)
end

BuffExtensionBase._is_valid_target = function (self, template)
	local buff_context = self._buff_context
	local is_player = buff_context.player and true or false
	local player_only = template.target == BUFF_TARGETS.player_only and is_player
	local minion_only = template.target == BUFF_TARGETS.minion_only and not is_player
	local any = template.target == nil or template.target == BUFF_TARGETS.any

	return player_only or minion_only or any
end

BuffExtensionBase._handle_unique_buffs = function (self, new_template)
	local can_add_buff = true
	local unique_buff_id = new_template.unique_buff_id

	if unique_buff_id then
		local buffs_by_index = self._buffs_by_index

		for index, buff_instance in pairs(buffs_by_index) do
			local template = buff_instance:template()
			local unique_id = template.unique_buff_id

			if unique_buff_id == unique_id then
				local new_buff_priority = new_template.unique_buff_priority
				local current_buff_priority = template.unique_buff_priority

				if not new_buff_priority or new_buff_priority <= current_buff_priority then
					self:_remove_internally_controlled_buff(index)

					break
				end

				can_add_buff = false

				break
			end
		end
	end

	return can_add_buff
end

BuffExtensionBase._check_max_stacks_cap = function (self, template, t)
	local max_stacks_cap = template.max_stacks_cap
	local template_name = template.name
	local buff_instance = self._stacking_buffs[template_name]

	if not max_stacks_cap or not buff_instance then
		return true
	end

	local stack_count = buff_instance:stack_count()

	if stack_count ~= max_stacks_cap then
		return true
	end

	if template.refresh_duration_on_stack or template.refresh_start_time_on_stack then
		buff_instance:set_start_time(t)
	end

	local previous_stack_count = stack_count

	buff_instance:refresh_func(t, previous_stack_count)
	self:_proc_on_buff_event(template_name, buff_instance, BuffSettings.proc_events.on_max_stack_refresh_buff)

	return false
end

BuffExtensionBase.refresh_duration_of_stacking_buff = function (self, buff_name, t)
	local buff_instance = self._stacking_buffs[buff_name]

	buff_instance:set_start_time(t)
end

BuffExtensionBase.add_duration_of_stacking_buff = function (self, buff_name, amount)
	local buff_instance = self._stacking_buffs[buff_name]

	buff_instance:add_duration(amount)
end

BuffExtensionBase.current_stacks = function (self, buff_name)
	local buff_instance = self._stacking_buffs[buff_name]

	return buff_instance and buff_instance:stack_count() or 0
end

BuffExtensionBase.buff_start_time = function (self, buff_name)
	local buff_instance = self._stacking_buffs[buff_name]

	return buff_instance and buff_instance:start_time()
end

BuffExtensionBase.buff_duration_progress = function (self, buff_name)
	local buff_instance = self._stacking_buffs[buff_name]

	return buff_instance and buff_instance:duration_progress() or 0
end

BuffExtensionBase.remove_externally_controlled_buff = function (self, local_index)
	ferror("Buff extension is using base implementation of remove_externally_controlled_buff, it shouldn't")
end

BuffExtensionBase._remove_internally_controlled_buff = function (self, local_index)
	ferror("Buff extension is using base implementation of _remove_internally_controlled_buff, it shouldn't")
end

BuffExtensionBase._remove_buff = function (self, index, extension_destroyed)
	local buffs_by_index = self._buffs_by_index
	local buff_instance = buffs_by_index[index]

	if not buff_instance or buff_instance.__deleted then
		Log.exception("BuffExtensionBase", "Tried removing buff that had already been deleted. %d", index)

		self._buffs_by_index[index] = nil

		return
	end

	local template = buff_instance:template()
	local current_stack_count = buff_instance:stack_count()

	if current_stack_count > 1 then
		buff_instance:remove_stack()
		self:_on_remove_buff_stack(buff_instance, current_stack_count)

		if template.refresh_duration_on_remove_stack then
			local max_stacks = buff_instance:max_stacks()

			if current_stack_count <= max_stacks then
				local t = FixedFrame.get_latest_fixed_time()

				buff_instance:set_start_time(t)
			end
		end
	else
		local can_stack = template.max_stacks and true or false

		if can_stack then
			local template_name = template.name

			self._stacking_buffs[template_name] = nil
		end

		local buffs = self._buffs
		local instance_index

		for i = 1, #buffs do
			local instance = buffs[i]

			if instance == buff_instance then
				instance_index = i

				break
			end
		end

		local instance_id = buff_instance:instance_id()

		if not self._is_hub then
			self:_stop_fx(instance_id, template)
		end

		self:_on_remove_buff(buff_instance)
		table.remove(buffs, instance_index)
		buff_instance:delete(extension_destroyed)
		self:_post_on_remove_buff(buff_instance)
	end

	self._buffs_by_index[index] = nil
end

BuffExtensionBase._on_add_buff = function (self, buff_instance)
	return
end

BuffExtensionBase._on_remove_buff = function (self, buff_instance)
	return
end

BuffExtensionBase._post_on_remove_buff = function (self, buff_instance)
	return
end

BuffExtensionBase._on_remove_buff_stack = function (self, buff_instance, previous_stack_count)
	local template = buff_instance:template()
	local shared_effects = template.effects

	if shared_effects then
		local stack_node_effects = shared_effects.stack_node_effects

		if stack_node_effects then
			local template_name = template.name
			local current_stack_count = buff_instance:stack_count()

			self:_check_stack_node_effects(template_name, stack_node_effects, current_stack_count, previous_stack_count)
		end
	end
end

BuffExtensionBase._on_add_buff_stack = function (self, buff_instance, previous_stack_count)
	local template = buff_instance:template()
	local shared_effects = template.effects

	if shared_effects then
		local stack_node_effects = shared_effects.stack_node_effects

		if stack_node_effects then
			local template_name = template.name
			local current_stack_count = buff_instance:stack_count()

			self:_check_stack_node_effects(template_name, stack_node_effects, current_stack_count, previous_stack_count)
		end
	end

	local on_stack_added_func = template.on_stack_added_func

	if on_stack_added_func then
		local buff_context = buff_instance:template_context()
		local buff_data = buff_instance:template_data()
		local current_stack_count = buff_instance:stack_count()

		on_stack_added_func(buff_data, buff_context, current_stack_count)
	end
end

BuffExtensionBase.has_buff_using_buff_template = function (self, buff_template_name)
	local buffs = self._buffs

	for i = 1, #buffs do
		local buff_instance = buffs[i]
		local instance_template = buff_instance:template()

		if buff_template_name == instance_template.name then
			return true
		end
	end

	return false
end

BuffExtensionBase.has_buff_using_buff_template_with_owner = function (self, buff_template_name, owner_unit)
	local buffs = self._buffs

	for i = 1, #buffs do
		local buff_instance = buffs[i]
		local instance_template = buff_instance:template()
		local buff_context = buff_instance:template_context()
		local buff_owner_unit = buff_context.owner_unit

		if buff_template_name == instance_template.name and buff_owner_unit == owner_unit then
			return true
		end
	end

	return false
end

BuffExtensionBase.has_buff_using_buff_templates = function (self, ...)
	local buffs = self._buffs
	local num_templates = select("#", ...)

	for buff_i = 1, #buffs do
		local buff_instance = buffs[buff_i]
		local buff_template_name = buff_instance:template().name

		for select_i = 1, num_templates do
			if select(select_i, ...) == buff_template_name then
				return true
			end
		end
	end

	return false
end

BuffExtensionBase.has_active_buff_with_buff_template = function (self, buff_template_name)
	local any_active = false
	local buffs = self._buffs

	for buff_i = 1, #buffs do
		local buff_instance = buffs[buff_i]
		local instance_template = buff_instance:template()

		if buff_template_name == instance_template.name and buff_instance:is_proc_active() then
			any_active = true

			break
		end
	end

	return any_active
end

BuffExtensionBase.has_buff_id = function (self, buff_id)
	local buffs = self._buffs

	for i = 1, #buffs do
		local buff_instance = buffs[i]
		local intance_template = buff_instance:template()
		local instance_buff_name = intance_template.buff_id

		if instance_buff_name == buff_id then
			return true
		end
	end

	return false
end

BuffExtensionBase.has_unique_buff_id = function (self, unique_buff_id)
	local buffs = self._buffs

	for i = 1, #buffs do
		local buff_instance = buffs[i]
		local intance_template = buff_instance:template()
		local instance_buff_id = intance_template.unique_buff_id

		if instance_buff_id == unique_buff_id then
			return true
		end
	end

	return false
end

BuffExtensionBase.has_buff_id_with_owner = function (self, buff_id, owner_unit)
	local buffs = self._buffs

	for i = 1, #buffs do
		local buff_instance = buffs[i]
		local intance_template = buff_instance:template()
		local instance_buff_name = intance_template.buff_id
		local buff_context = buff_instance:template_context()
		local buff_owner_unit = buff_context.owner_unit

		if instance_buff_name == buff_id and buff_owner_unit == owner_unit then
			return true
		end
	end

	return false
end

BuffExtensionBase.owner_of_buff_with_id = function (self, buff_id)
	local buffs = self._buffs

	for i = 1, #buffs do
		local buff_instance = buffs[i]
		local intance_template = buff_instance:template()
		local instance_buff_name = intance_template.buff_id
		local buff_context = buff_instance:template_context()
		local buff_owner_unit = buff_context.owner_unit

		if instance_buff_name == buff_id then
			return buff_owner_unit
		end
	end

	return nil
end

BuffExtensionBase.has_keyword = function (self, keyword)
	return not not self._keywords[keyword]
end

BuffExtensionBase.has_any_keyword = function (self, keywords)
	local active_keywords = self._keywords

	for ii = 1, #keywords do
		if active_keywords[keywords[ii]] then
			return true
		end
	end

	return false
end

BuffExtensionBase.had_keyword = function (self, keyword, interval)
	local added_t = self._had_keywords[keyword]

	if not added_t then
		return false
	end

	interval = interval or 1

	local t = FixedFrame.get_latest_fixed_time()

	return interval > t - added_t
end

BuffExtensionBase.keywords = function (self)
	return self._keywords
end

BuffExtensionBase.stat_buffs = function (self)
	return self._stat_buffs
end

BuffExtensionBase.buffs = function (self)
	return self._buffs
end

BuffExtensionBase.portable_random = function (self)
	return self._portable_random
end

BuffExtensionBase.set_frame_unique_proc = function (self, event, unique_key)
	if not self._unique_frame_proc[event] then
		self._unique_frame_proc[event] = {}
	end

	self._unique_frame_proc[event][unique_key] = true
end

BuffExtensionBase.is_frame_unique_proc = function (self, event, unique_key)
	return not self._unique_frame_proc[event] or not self._unique_frame_proc[event][unique_key]
end

BuffExtensionBase.request_proc_event_param_table = function (self)
	local num_params_table_in_use = self._num_params_table_in_use + 1

	if num_params_table_in_use > MAX_PROC_EVENTS then
		Log.warning("BuffExtensionBase", "Out of proc event tables, ignoring proc!")

		return nil
	end

	local param_table_index = self._param_tables_start_index_reference
	local starting_index = param_table_index - 1
	local current_table_index = (starting_index + num_params_table_in_use - 1) % MAX_PROC_EVENTS + 1
	local param_table = self._proc_event_param_tables[current_table_index]

	if not param_table then
		param_table = Script.new_map(32)
		self._proc_event_param_tables[current_table_index] = param_table
	end

	self._num_params_table_in_use = num_params_table_in_use

	return param_table
end

BuffExtensionBase.add_proc_event = function (self, event, params)
	local num_proc_events = self._num_proc_events

	params.triggering_proc_event = event

	local proc_events = self._proc_events

	proc_events[num_proc_events * PROC_EVENTS_STRIDE + 1] = event
	proc_events[num_proc_events * PROC_EVENTS_STRIDE + 2] = params
	self._num_proc_events = num_proc_events + 1
end

BuffExtensionBase._find_local_index = function (self, buff_instance)
	for local_index, indexed_buff_instance in pairs(self._buffs_by_index) do
		if indexed_buff_instance == buff_instance then
			return local_index
		end
	end

	return nil
end

BuffExtensionBase._start_fx = function (self, index, template)
	local shared_effects = template.effects

	if shared_effects then
		local node_effects = shared_effects.node_effects

		if node_effects then
			local template_name = template.name

			self:_start_node_effects(template_name, node_effects)
		end
	end
end

BuffExtensionBase._stop_fx = function (self, index, template)
	local shared_effects = template.effects

	if shared_effects then
		local shared_node_effects = shared_effects.node_effects

		if shared_node_effects then
			local template_name = template.name

			self:_stop_node_effects(template_name, shared_node_effects)
		end
	end
end

BuffExtensionBase._start_node_effects = function (self, template_name, node_effects, optional_node_effects_priotity)
	local active_node_sfx_effects = self._sfx_node_effects
	local active_node_vfx_effects = self._vfx_node_effects
	local new_node_effect_priority = optional_node_effects_priotity or 1
	local buff_context = self._buff_context
	local world = buff_context.world
	local wwise_world = buff_context.wwise_world
	local unit = buff_context.unit
	local vfx_material_emission_applied = false
	local num_effects = #node_effects

	for i = 1, num_effects do
		repeat
			local effect = node_effects[i]
			local node_name = effect.node_name

			if not Unit_has_node(unit, node_name) then
				break
			end

			local node_index = Unit_node(unit, node_name)
			local node_position = Unit_world_position(unit, node_index)
			local sfx = effect.sfx

			if sfx then
				if not active_node_sfx_effects[node_index] then
					local wwise_source_id = WwiseWorld.make_manual_source(wwise_world, node_position, Quaternion.identity())

					active_node_sfx_effects[node_index] = {
						wwise_source_id = wwise_source_id,
						active_wwise_events = {},
					}
				end

				local active_node_source = active_node_sfx_effects[node_index]
				local wwise_source_id = active_node_source.wwise_source_id
				local active_wwise_events = active_node_source.active_wwise_events
				local looping_wwise_start_event = sfx.looping_wwise_start_event
				local ref_count = active_wwise_events[looping_wwise_start_event]

				if not ref_count then
					WwiseWorld.trigger_resource_event(wwise_world, looping_wwise_start_event, wwise_source_id)
				end

				active_wwise_events[looping_wwise_start_event] = (ref_count or 0) + 1
			end

			local vfx = effect.vfx

			if vfx then
				if not active_node_vfx_effects[node_index] then
					active_node_vfx_effects[node_index] = {}
				end

				local particle_effect = vfx.particle_effect
				local active_node_vfx = active_node_vfx_effects[node_index]

				if not active_node_vfx[particle_effect] then
					local effect_id = World.create_particles(world, particle_effect, node_position)

					if vfx.material_emission then
						local mesh_name_or_nil = vfx.emission_mesh_name
						local material_name_or_nil = vfx.emission_material_name
						local apply_for_children = true

						World.set_particles_surface_effect(world, effect_id, unit, mesh_name_or_nil, material_name_or_nil, apply_for_children)

						vfx_material_emission_applied = true
					else
						local orphaned_policy = vfx.orphaned_policy or "destroy"

						World.link_particles(world, effect_id, unit, node_index, Matrix4x4.identity(), orphaned_policy)
					end

					local stop_type = vfx.stop_type or "destroy"

					active_node_vfx[particle_effect] = {
						particle_id = effect_id,
						stop_type = stop_type,
						current_effect_template_name = template_name,
						current_effect_priority = new_node_effect_priority,
						active_buffs = {
							[template_name] = {
								active_buffs_count = 1,
								material_variables = vfx.material_variables or nil,
								priority = new_node_effect_priority,
							},
						},
					}

					local material_variables = vfx.material_variables

					if material_variables then
						for j = 1, #material_variables do
							local entry = material_variables[j]
							local material_name = entry.material_name
							local variable_name = entry.variable_name
							local value = entry.value
							local vector_value = Vector3(value[1], value[2], value[3])

							World.set_particles_material_vector3(world, effect_id, material_name, variable_name, vector_value)
						end
					end
				else
					local active_particle_effect_data = active_node_vfx[particle_effect]
					local current_effect_priority = active_particle_effect_data.current_effect_priority
					local active_effects_for_node_and_particle = active_particle_effect_data.active_buffs
					local new_buff_existing_data = active_effects_for_node_and_particle[template_name]

					if new_buff_existing_data then
						new_buff_existing_data.active_buffs_count = new_buff_existing_data.active_buffs_count + 1
					else
						active_effects_for_node_and_particle[template_name] = {
							active_buffs_count = 1,
							material_variables = vfx.material_variables or nil,
							priority = new_node_effect_priority,
						}
					end

					if current_effect_priority <= new_node_effect_priority then
						self:_set_new_effect_for_active_node_particle_effect(world, active_particle_effect_data, template_name, new_node_effect_priority, vfx.material_variables)
					end
				end

				local active_particle_node_effect = active_node_vfx[particle_effect]
				local new_ref_count = (active_particle_node_effect.ref_count or 0) + 1

				active_particle_node_effect.ref_count = new_ref_count
			end
		until true
	end

	local is_minion = not buff_context.player

	if vfx_material_emission_applied and is_minion and not ScriptUnit.extension(unit, "visual_loadout_system"):ailment_effect() then
		Unit.set_vector3_for_materials(unit, "offset_time_duration", Vector3(0, 0, -10), true)
	end
end

BuffExtensionBase._stop_node_effects = function (self, removed_buff_template_name, node_effects)
	local active_node_sfx_effects = self._sfx_node_effects
	local active_node_vfx_effects = self._vfx_node_effects
	local buff_context = self._buff_context
	local wwise_world = buff_context.wwise_world
	local world = buff_context.world

	for i = 1, #node_effects do
		repeat
			local effect = node_effects[i]
			local node_name = effect.node_name

			if not Unit_has_node(self._unit, node_name) then
				break
			end

			local node_index = Unit_node(self._unit, node_name)
			local sfx = effect.sfx

			if sfx then
				local active_node_sfx = active_node_sfx_effects[node_index]
				local looping_wwise_start_event = sfx.looping_wwise_start_event
				local active_wwise_events = active_node_sfx.active_wwise_events
				local new_ref_count = active_wwise_events[looping_wwise_start_event] - 1

				active_wwise_events[looping_wwise_start_event] = new_ref_count

				local wwise_source_id = active_node_sfx.wwise_source_id

				if new_ref_count < 1 then
					local looping_wwise_stop_event = sfx.looping_wwise_stop_event

					WwiseWorld.trigger_resource_event(wwise_world, looping_wwise_stop_event, wwise_source_id)

					active_wwise_events[looping_wwise_start_event] = nil
				end

				if next(active_wwise_events) == nil then
					WwiseWorld.destroy_manual_source(wwise_world, wwise_source_id)

					active_node_sfx_effects[node_index] = nil
				end
			end

			local vfx = effect.vfx

			if vfx then
				local active_node_vfx = active_node_vfx_effects[node_index]
				local particle_effect = vfx.particle_effect
				local active_particle_effect_data = active_node_vfx[particle_effect]
				local new_ref_count = (active_particle_effect_data.ref_count or 0) - 1

				active_particle_effect_data.ref_count = new_ref_count

				if new_ref_count < 1 then
					local particle_id = active_particle_effect_data.particle_id
					local stop_type = active_particle_effect_data.stop_type

					if stop_type == "stop" then
						World.stop_spawning_particles(world, particle_id)
					else
						World.destroy_particles(world, particle_id)
					end

					table.clear(active_node_vfx[particle_effect].active_buffs)

					active_node_vfx[particle_effect] = nil
				else
					local active_buffs_for_node_and_particle = active_particle_effect_data.active_buffs
					local removed_buff_data = active_buffs_for_node_and_particle[removed_buff_template_name]

					removed_buff_data.active_buffs_count = removed_buff_data.active_buffs_count - 1

					local last_instance_of_buff_removed = removed_buff_data.active_buffs_count < 1

					if last_instance_of_buff_removed then
						active_buffs_for_node_and_particle[removed_buff_template_name] = nil

						self:_start_next_highest_priority_active_node_effect(world, removed_buff_template_name, active_particle_effect_data)
					end
				end

				if next(active_node_vfx) == nil then
					active_node_vfx_effects[node_index] = nil
				end
			end
		until true
	end
end

BuffExtensionBase._start_next_highest_priority_active_node_effect = function (self, world, removed_buff_template_name, active_particle_effect_data)
	local active_buffs_for_node_and_particle = active_particle_effect_data.active_buffs
	local new_node_effect_priority = -1
	local new_node_effect_template_name, new_node_effect_material_variables

	for active_buff_name, active_effect_data in pairs(active_buffs_for_node_and_particle) do
		if new_node_effect_priority < active_effect_data.priority or active_effect_data.priority == new_node_effect_priority and new_node_effect_material_variables == nil then
			new_node_effect_priority = active_effect_data.priority
			new_node_effect_material_variables = active_effect_data.material_variables
			new_node_effect_template_name = active_buff_name
		end
	end

	if new_node_effect_template_name then
		self:_set_new_effect_for_active_node_particle_effect(world, active_particle_effect_data, new_node_effect_template_name, new_node_effect_priority, new_node_effect_material_variables)
	end
end

BuffExtensionBase._set_new_effect_for_active_node_particle_effect = function (self, world, active_particle_effect_data, new_effect_template_name, new_effect_priority, new_effect_material_variables)
	active_particle_effect_data.current_effect_template_name = new_effect_template_name
	active_particle_effect_data.current_effect_priority = new_effect_priority

	local particle_id = active_particle_effect_data.particle_id

	if new_effect_material_variables and particle_id then
		for j = 1, #new_effect_material_variables do
			local entry = new_effect_material_variables[j]
			local material_name = entry.material_name
			local variable_name = entry.variable_name
			local value = entry.value
			local vector_value = Vector3(value[1], value[2], value[3])

			World.set_particles_material_vector3(world, particle_id, material_name, variable_name, vector_value)
		end
	end
end

local TEMP_STACK_NODE_EFFECTS = {}

BuffExtensionBase._check_stack_node_effects = function (self, template_name, stack_node_effects, current_stack_count, previous_stack_count, optional_node_effects_priotity)
	if previous_stack_count < current_stack_count then
		local index = 0

		for required_stack_count, node_effects in pairs(stack_node_effects) do
			local should_start_effect = previous_stack_count < required_stack_count and required_stack_count <= current_stack_count

			if should_start_effect then
				for i = 1, #node_effects do
					index = index + 1

					local effect = node_effects[i]

					TEMP_STACK_NODE_EFFECTS[index] = effect
				end
			end
		end

		if index > 0 then
			self:_start_node_effects(template_name, TEMP_STACK_NODE_EFFECTS, optional_node_effects_priotity or 1)
			table.clear_array(TEMP_STACK_NODE_EFFECTS, index)
		end
	else
		local index = 0

		for required_stack_count, node_effects in pairs(stack_node_effects) do
			local should_stop_effect = required_stack_count <= previous_stack_count and current_stack_count < required_stack_count

			if should_stop_effect then
				for i = 1, #node_effects do
					index = index + 1

					local effect = node_effects[i]

					TEMP_STACK_NODE_EFFECTS[index] = effect
				end
			end
		end

		if index > 0 then
			self:_stop_node_effects(template_name, TEMP_STACK_NODE_EFFECTS)
			table.clear_array(TEMP_STACK_NODE_EFFECTS, index)
		end
	end
end

BuffExtensionBase._proc_on_buff_event = function (self, template_name, buff_instance, proc_event_name)
	local param_table = self:request_proc_event_param_table()

	if param_table then
		param_table.template_name = template_name
		param_table.unit = self._unit

		self:add_proc_event(proc_event_name, param_table)
	end

	local additional_arguments = buff_instance and buff_instance:additional_arguments()

	if additional_arguments then
		local owner_unit = additional_arguments.owner_unit
		local owner_buff_extension = owner_unit and ScriptUnit.has_extension(owner_unit, "buff_system")
		local owner_param_table = owner_buff_extension and owner_buff_extension:request_proc_event_param_table()

		if owner_param_table then
			owner_param_table.template_name = template_name
			owner_param_table.unit = self._unit

			owner_buff_extension:add_proc_event(proc_event_name, owner_param_table)
		end
	end
end

BuffExtensionBase._clear_param_tables = function (self, number_of_elements_to_clear)
	local param_table_index = self._param_tables_start_index_reference
	local starting_index = param_table_index - 1
	local current_index

	for i = 1, number_of_elements_to_clear do
		current_index = (starting_index + i - 1) % MAX_PROC_EVENTS + 1

		table.clear(self._proc_event_param_tables[current_index])
	end

	self._param_tables_start_index_reference = (param_table_index + number_of_elements_to_clear - 1) % MAX_PROC_EVENTS + 1
end

BuffExtensionBase.rpc_add_buff = function (self, channel_id, game_object_id, buff_template_id, server_index, optional_lerp_value, optional_item_slot_id, optional_parent_buff_template_id, from_talent)
	local template_name = NetworkLookup.buff_templates[buff_template_id]
	local template = BuffTemplates[template_name]
	local t = FixedFrame.get_latest_fixed_time()
	local optional_item_slot_name = optional_item_slot_id and NetworkLookup.player_inventory_slot_names[optional_item_slot_id]
	local optional_parent_buff_template = optional_parent_buff_template_id and NetworkLookup.buff_templates[optional_parent_buff_template_id]
	local index = self:_add_buff(template, t, "buff_lerp_value", optional_lerp_value, "item_slot_name", optional_item_slot_name, "parent_buff_template", optional_parent_buff_template, "from_talent", from_talent)

	self._buff_index_map[server_index] = index
end

BuffExtensionBase.rpc_add_buff_with_stacks = function (self, channel_id, game_object_id, buff_template_id, server_index_array, optional_lerp_value, optional_item_slot_id, optional_parent_buff_template_id)
	local template_name = NetworkLookup.buff_templates[buff_template_id]
	local template = BuffTemplates[template_name]
	local t = FixedFrame.get_latest_fixed_time()
	local optional_item_slot_name = optional_item_slot_id and NetworkLookup.player_inventory_slot_names[optional_item_slot_id]
	local optional_parent_buff_template = optional_parent_buff_template_id and NetworkLookup.buff_templates[optional_parent_buff_template_id]
	local num_index = #server_index_array

	if num_index == 0 then
		Log.exception("BuffExtensionBase", "Got rpc_add_buff_with_stacks with #server_index_array == 0")
	end

	for i = 1, num_index do
		local server_index = server_index_array[i]
		local index = self:_add_buff(template, t, "buff_lerp_value", optional_lerp_value, "item_slot_name", optional_item_slot_name, "parent_buff_template", optional_parent_buff_template)

		self._buff_index_map[server_index] = index
	end
end

BuffExtensionBase.rpc_remove_buff = function (self, channel_id, game_object_id, server_index)
	local index = self._buff_index_map[server_index]

	if not index then
		Log.exception("BuffExtensionBase", "Got rpc_remove_buff for buff that doesn't exist or already deleted, index == nil")

		return
	end

	self:_remove_buff(index)

	self._buff_index_map[server_index] = nil
end

BuffExtensionBase.rpc_buff_proc_set_active_time = function (self, channel_id, game_object_id, server_index, activation_frame)
	local index = self._buff_index_map[server_index]
	local activation_time = activation_frame * self._fixed_time_step

	self:_set_proc_active_start_time(index, activation_time)
end

BuffExtensionBase._set_proc_active_start_time = function (self, index, activation_time, skip_send_active_time_rpc)
	ferror("Buff extension is using base implementation of _set_proc_active_start_time, it shouldn't")
end

BuffExtensionBase.rpc_buff_set_start_time = function (self, channel_id, game_object_id, server_index, activation_frame)
	local index = self._buff_index_map[server_index]
	local start_time = activation_frame * self._fixed_time_step

	self:_set_start_time_from_rpc(index, start_time)
end

BuffExtensionBase._set_start_time_from_rpc = function (self, index, start_time)
	ferror("Buff extension is using base implementation of _set_start_time_from_rpc, it shouldn't")
end

implements(BuffExtensionBase, BuffExtensionInterface)

return BuffExtensionBase
