-- Linear inter/extrapolation
local function lerp(a, b, f)
    if a and b and f then
        return a + f * (b - a)
    else
        return a or b or 0
    end
end

local ini_eff = ini_file("plugins\\grok_bo_enhanced_recoil.ltx")
local ini_eff_og = ini_file("plugins\\actor_effects.ltx")

local wpn_fx = {}
local function parse_anims(curr_ini_eff, sec, add_default)
	if not curr_ini_eff then
		return
	end

	if wpn_fx[sec] then
		return
	end

	local sec_p = ini_sys:r_string_ex(sec,"parent_section") or sec
	
	if wpn_fx[sec_p] and (sec ~= sec_p) then
		wpn_fx[sec] = wpn_fx[sec_p]
		
	elseif curr_ini_eff:section_exist(sec_p) then
		if not (
			curr_ini_eff:r_string_ex(sec_p,"s")
		-- and curr_ini_eff:r_string_ex(sec_p,"r")
		and curr_ini_eff:r_string_ex(sec_p,"e")
		)
		then
			return
		end

		local tbl = {}
		
		-- parse strength
		tbl.s = {}
		local s = curr_ini_eff:r_string_ex(sec_p,"s")
		local _s = str_explode(s,",")
		for i=1,#_s do
			tbl.s[#tbl.s + 1] = tonumber(_s[i])
		end
		
		-- parse strength randomizer
		tbl.r = curr_ini_eff:r_float_ex(sec_p,"r")
		
		-- parse anm
		tbl.e = {}
		local e = curr_ini_eff:r_string_ex(sec_p,"e")
		local _e = str_explode(e,",")
		for i=1,#_e do
			local __e = str_explode(_e[i],":")
			for j=1,#__e do
				if not ((i == 1 or i == 4) and j >= 2) then
					__e[j] = tonumber(__e[j])
				end
			end
			tbl.e[#tbl.e + 1] = __e
		end
		
		-- parse ppe
		tbl.p = {}
		local p = curr_ini_eff:r_string_ex(sec_p,"p")
		if p and p ~= "" then
			for k,v in string.gmatch(p, "([%w_%-%s%.]+)=([%w_%-%s%.]+)") do
				tbl.p[k] = tonumber(v)
			end
		end
		
		if (not wpn_fx[sec_p]) and (sec ~= sec_p) then
			wpn_fx[sec_p] = tbl
		end
		
		wpn_fx[sec] = tbl
		--utils_data.print_table(wpn_fx[sec])
		
	else
		wpn_fx[sec] = add_default and wpn_fx["wpn_aug"] or true
		--printf("not recoil effect for [%s]", sec)
	end
end

-- Recoil modifier table
-- Contains functions that return modifier coefficients (see example)
modifier_table = {
	[1] = function(effect_power, wpn, wpn_fx)
		return 1
	end,
}

function modifier(effect_power, wpn)
	if is_empty(modifier_table) then
		return effect_power
	end
	for i, v in ipairs(modifier_table) do
		effect_power = effect_power * (v(effect_power, wpn, wpn_fx[wpn:section()]) or 1)
	end
	return effect_power
end

-- Add modifier, modifier_func is function as in modifier_table, pos is position to add (optional)
function add_modifier(modifier_func, pos)
	pos = pos and clamp(pos, 1, #modifier_table + 1) or (#modifier_table + 1)
	table.insert(modifier_table, pos, modifier_func)
end

function remove_modifier(modifier_func)
	for i = #modifier_table, 1, -1 do
		local v = modifier_table[i]
		if modifier_func == v then
			table.remove(modifier_table, i)
		end
	end
end

function clamp_effect_power(effect_power)
	return math.max(0.15, effect_power)
end

local effect_min_id = 5000
local effect_max_id = 30000
local effect_id = effect_min_id
function next_effect_id()
	effect_id = effect_id + 1
	if effect_id > effect_max_id then
		effect_id = effect_min_id
	end
end

function add_cam_effector(filename, anim_id, cyclic, callback_str, cam_fov, hud, effect_power)
	local current_id = effect_id
	level.add_cam_effector(filename, effect_id, cyclic, callback_str, cam_fov, hud, effect_power)
	next_effect_id()
	return current_id
end

function add_pp_effector(filename, anim_id, cyclic)
	local current_id = effect_id
	level.add_pp_effector(filename, effect_id, cyclic)
	next_effect_id()
	return current_id
end

local shotc = 1
local shott = 0
local fov_r = 1
abakan_state = 0
function shoot_effect(sect)
	--printf("-SHOOT")

	if shott and (shott < time_global()) then
		shotc = 1
		shott = nil
	end

	local anims = {}
	local s = wpn_fx[sect].s
	local sc = #s

	if sc > 1 then
		if wpn_fx[sect].r == 1 then
			s = s[math.random(#s)]
		elseif wpn_fx[sect].r == 2 then
			if shotc == 1 then
				s = s[1]
			else
				s = s[math.random(2,#s)]
			end
		else
			sc = shotc <= sc and shotc or (sc > 2 and math.random(sc-1,sc)) or sc
			s = s[sc]
		end
	else
		s = s[1]
	end
--	local cnt, r = math.modf(s*fov_r)
--	if r ~=0 then
--		s = math.max(1, cnt - 1)
--	end
	for i,v in ipairs(wpn_fx[sect].e) do
		local cnt,r = math.modf(v[1]*fov_r)
		if r ~= 0 and cnt > 4 then --fmb
			cnt = cnt-cnt*math.random(0,r*100)/100 --fmb
			cnt,r = math.modf(cnt)
		end

		if r ~= 0 and math.random() < r then
			cnt = cnt+1
		end

		if cnt > 0 then
			table.insert(anims,{e = i,d = v[2] or math.random(0,1),c = cnt})
		end
	end

	-- Get weapon upgrades and reduce effect based on them
	local effect_power = settings.effect_power
	local wpn = db.actor:active_item()
	-- printf("base effect_power %s", effect_power)
	wpn:iterate_installed_upgrades( function(upgr_sec)
		-- printf("installed upgrade %s", upgr_sec)
		local section = SYS_GetParam(0, upgr_sec, "section")
		if not section then return end

		local upgrades_power = settings.upgrades_power or 1

		-- Upgrades reduces values, so effect power must be with "+"
		local cam_dispersion = SYS_GetParam(0, section, "cam_dispersion")
		cam_dispersion = cam_dispersion and tonumber(cam_dispersion) or 0
		effect_power = effect_power + cam_dispersion * upgrades_power

		local PDM_disp_base = SYS_GetParam(0, section, "PDM_disp_base")
		PDM_disp_base = PDM_disp_base and tonumber(PDM_disp_base) or 0
		effect_power = effect_power + PDM_disp_base * upgrades_power

		effect_power = clamp_effect_power(effect_power)
	end)

	-- Get actor state and increase/reduce effect
	-- Reduce recoil by 50% when proning
	if IsMoveState('mcCrouch') and IsMoveState('mcAccel') then
		effect_power = clamp_effect_power(effect_power * (1 - 0.5 * (settings.mcProne_power or 1)))
	-- Reduce recoil by 15% when crouching
	elseif IsMoveState('mcCrouch') then
		effect_power = clamp_effect_power(effect_power * (1 - 0.15 * (settings.mcCrouch_power or 1)))
	end

	-- Increase recoil by 20% when moving
	if IsMoveState('mcAnyMove') then
		effect_power = clamp_effect_power(effect_power * (1 + 0.2 * (settings.mcAnyMove_power or 1)))
	end

	-- Increase recoil by 25% when sprinting
	if IsMoveState('mcSprint') then
		effect_power = clamp_effect_power(effect_power * (1 + 0.25 * (settings.mcSprint_power or 1)))
	end

	-- Increase recoil by 25% when jumping and landing
	if IsMoveState('mcJump') 
	or IsMoveState('mcFall')
	or IsMoveState('mcLanding')
	or IsMoveState('mcLanding2') then
		effect_power = clamp_effect_power(effect_power * (1 + 0.25 * (settings.mcJump_power or 1)))
	end

	-- Reduce recoil by 5% when zooming
	if axr_main.weapon_is_zoomed then
		effect_power = clamp_effect_power(effect_power * (1 - 0.05 * (settings.mcZoom_power or 1)))
	end

	-- Clamp result
	effect_power = clamp_effect_power(effect_power)

	-- Condition modifier
	if settings.condition_power > 0 then
		local c = wpn:condition()
		local c_threshold = 0.93
		local m
		if c >= c_threshold then
			m = lerp(0, -0.12, normalize(c, c_threshold, 1)) * settings.condition_power
		else
			m = lerp(0.85, 0, normalize(c, 0, c_threshold)) * settings.condition_power
		end
		effect_power = clamp_effect_power(effect_power * (1 + m))
	end

	-- External modifiers for recoil
	effect_power = clamp_effect_power(modifier(effect_power, wpn))

	-- Abakan delayed effect
	if ( -- If weapon is Abakan
		wpn:section():find("abakan")
		or wpn:section():find("an_94")
		or wpn:section():find("an94")
	)
	-- If Abakan has > 1 ammo or is in second shot state
	and (wpn:cast_Weapon():GetAmmoElapsed() > 1 or abakan_state == 1)
	-- If Abakan fire mode is not single shot 
	and wpn:cast_Weapon():GetFireMode() ~= 1
	-- Check for 2 shot burst fire mode if "cycle_down" is false (vanilla behaviour, BAN's Abakan replacer works correctly to IRL)
	and (SYS_GetParam(1, wpn:section(), "cycle_down") or wpn:cast_Weapon():GetFireMode() == 2)
	then
		-- First shot - 0 recoil
		if abakan_state == 0 then
			effect_power = 0.0000001
			abakan_state = 1
		-- Second shot - double recoil power
		elseif abakan_state == 1 then
			effect_power = effect_power * 2
			abakan_state = 2
		-- Consecutive shots - single recoil power
		elseif abakan_state == 2 then
			-- effect_power = effect_power
			-- abakan_state = 2
		end

		-- Reset state after some time
		-- Add small epsilon to timer for better behaviour on full auto consecutive shots
		local state = "abakan_state_reset"
		local state_id = "abakan_state_reset"
		local timer = 1 / (SYS_GetParam(2, wpn:section(), "rpm") / 60) + 0.001
		ResetTimeEvent(state, state_id, timer)
		CreateTimeEvent(state, state_id, timer, function()
			abakan_state = 0
			return true
		end)
	end

	-- printf("new effect_power %s", effect_power)

	for i,a in ipairs(anims) do
		local n = string.format([[shoot\s%s_e%s_%s.anm]],s,a.e,a.d)
		for ii=1,a.c do
			local anim_id = 0
			add_cam_effector(n,anim_id,false,"", 0, true, effect_power)
			if settings.gun_effect_power > 0 then
				add_cam_effector(n,anim_id + 1,false,"", 0, false, effect_power * 0.17 * settings.gun_effect_power)
			end
		end
	end
	for k,v in pairs(wpn_fx[sect].p) do
		local eid = 0
		eid = add_pp_effector(string.format([[shoot\%s_s%s.ppe]],k,s), eid, false)
		v = math.max(0.0000001, v * settings.pp_power)
		level.set_pp_effector_factor(eid, v)
	end
	shotc = shotc+1
	shott = time_global()+100
	
	if settings.fov_changer then
		local fov_before = get_console_cmd(2, "fov")
		local weapon_shake_factor = s * 0.1 + 1
		exec_console_cmd("fov " .. clamp(math.atan(math.tan(fov_before * (0.5 * math.pi / 180)) * (axr_main.weapon_is_zoomed and 1.1 or 1.05 * weapon_shake_factor ) * settings.fov_power) / (0.5 * math.pi / 180), 5, 120))
		
		local function reset_fov_to_normal(fov_val)
			exec_console_cmd("fov " .. fov_val)
			return true
		end
		
		CreateTimeEvent(0, "reset_fov_to_normal", 0.075, reset_fov_to_normal, fov_before)
	end
end


function Update_Shooting(obj, wpn, ammo_elapsed, grenade_elapsed, ammo_type, grenade_type)
	if (obj:id() ~= AC_ID) then
		return
	end

	local wpn_obj = utils_item.item_is_fa(wpn) and wpn
	if wpn_obj then
		local sec = wpn_obj:section()
		
		if (not wpn_fx[sec]) then
			parse_anims(ini_eff, sec, true)
			parse_anims(ini_eff_og, sec, true)
		end
		
		if (wpn_fx[sec] == true) then
			return
		end
		
		shoot_effect(sec)
		
		--printf("Shooting effect played for [%s]", sec)
	end
end

-- MCM
function load_defaults()
	local t = {}
	local op = grok_bo_enhanced_recoil_mcm.op
	for i, v in ipairs(op.gr) do
		if v.def ~= nil then
			t[v.id] = v.def
		end
	end
	return t
end

settings = load_defaults()

function load_settings()
	settings = load_defaults()
	if ui_mcm then
		for k, v in pairs(settings) do
			settings[k] = ui_mcm.get(grok_bo_enhanced_recoil_mcm.op_id .. "/" .. k)
		end
	end
	return settings
end

function on_option_change()
	load_settings()
	if settings.enabled then
		RegisterScriptCallback("actor_on_weapon_fired", Update_Shooting)
	else
		UnregisterScriptCallback("actor_on_weapon_fired", Update_Shooting)
	end
end

function actor_on_first_update()
	on_option_change()
	ini_eff:section_for_each(function(sec)
		parse_anims(ini_eff, sec)
	end)
	ini_eff_og:section_for_each(function(sec)
		parse_anims(ini_eff_og, sec)
	end)
end

function on_game_start()
	RegisterScriptCallback("on_option_change", on_option_change)
	RegisterScriptCallback("actor_on_first_update", actor_on_first_update)
	RegisterScriptCallback("actor_on_weapon_fired", Update_Shooting)
end