local S = minetest.get_translator(minetest.get_current_modname())

--[[
if not minetest.settings:get_bool("enable_damage") then
	minetest.log("warning", "[stamina] Stamina will not load if damage is disabled (enable_damage=false)")
	return
end
]]--

stamina = {}
local modname = minetest.get_current_modname()

function stamina.log(level, message, ...)
	return minetest.log(level, ("[%s] %s"):format(modname, message:format(...)))
end

local function get_setting(key, default)
	local value = minetest.settings:get("stamina." .. key)
	local num_value = tonumber(value)
	if value and not num_value then
		stamina.log("warning", "Invalid value for setting %s: %q. Using default %q.", key, value, default)
	end
	return num_value or default
end

stamina.settings = {
	-- see settingtypes.txt for descriptions
	eat_particles = minetest.settings:get_bool("stamina.eat_particles", true),
	stamina_tick = get_setting("tick", 1),
	tick_min = get_setting("tick_min", 4),
	health_tick = get_setting("health_tick", 5),
	move_tick = get_setting("move_tick", 0.1),
	poison_tick = get_setting("poison_tick", 2.0),
	exhaust_dig = get_setting("exhaust_dig", 3),
	exhaust_place = get_setting("exhaust_place", 1),
	exhaust_move = get_setting("exhaust_move", 0.3),
	exhaust_lvl = get_setting("exhaust_lvl", 160),
	exhaust_tick = 0.2,
	heal = get_setting("heal", 1),
	heal_lvl = get_setting("heal_lvl", 5),
	starve = get_setting("starve", 1),
	starve_lvl = get_setting("starve_lvl", 1),
	initial_saturation = 20,
	visual_max = get_setting("visual_max", 30),
}
local settings = stamina.settings

local attribute = {
	saturation = "stamina:level",
	poisoned = "stamina:poisoned",
	exhaustion = "stamina:exhaustion",
	heal_carryover = "stamina:heal_carryover",
	heal_cooldown = "stamina:heal_cooldown",
	starving_cooldown = "stamina:starving_cooldown",
	starve_buildup = "stamina:starve_buildup"
}

local function is_player(player)
	return (
		minetest.is_player(player) and
		not player.is_fake_player
	)
end

local function set_player_attribute(player, key, value)
	local meta = player:get_meta()
	if value == nil then
		meta:set_string(key, "")
	else
		meta:set_string(key, tostring(value))
	end
end

local function get_player_attribute(player, key)
	local meta = player:get_meta()
	return meta:get_string(key)
end

local hud_ids_by_player_name = {}

local function get_hud_id(player)
	return hud_ids_by_player_name[player:get_player_name()]
end

local function set_hud_id(player, hud_id)
	hud_ids_by_player_name[player:get_player_name()] = hud_id
end

--- SATURATION API ---
function stamina.get_saturation(player)
	return tonumber(get_player_attribute(player, attribute.saturation))
end

function stamina.set_saturation(player, level)
	local old = stamina.get_saturation(player) or settings.initial_saturation

	set_player_attribute(player, attribute.saturation, level)
	hb.change_hudbar(player, "hunger", level)

	-- Change the text color if satiation is too low to warn the player
	-- or if satiation recovered enough; only do this if necessary to reduce
	-- network overhead
	local heal_lvl = settings.heal_lvl
	local new_color = nil

	if old >= heal_lvl and level < heal_lvl then
		if level == 0 then
			new_color = 0xFF0000
		else
			new_color = 0xFF4500
		end
	elseif old == 0 and level > 0 then
		if level < heal_lvl then
			new_color = 0xFF4500
		else
			new_color = 0xFFFFFF
		end
	elseif old > 0 and level == 0 then
		new_color = 0xFF0000
	elseif old < heal_lvl and level >= heal_lvl then
		new_color = 0xFFFFFF
	end

	if new_color then
		hb.change_hudbar(player, "hunger", nil, nil, nil, nil, nil, nil, new_color)
	end
end

stamina.registered_on_update_saturations = {}
function stamina.register_on_update_saturation(fun)
	table.insert(stamina.registered_on_update_saturations, fun)
end

function stamina.update_saturation(player, level)
	for _, callback in ipairs(stamina.registered_on_update_saturations) do
		local result = callback(player, level)
		if result then
			return result
		end
	end

	local old = stamina.get_saturation(player)

	if level == old then -- To suppress HUD update
		return
	end

	-- players without interact priv cannot eat
	if old < settings.heal_lvl and not minetest.check_player_privs(player, {interact=true}) then
		return
	end

	stamina.set_saturation(player, level)
end

function stamina.change_saturation(player, change)
	if not is_player(player) or not change or change == 0 then
		return false
	end
	local level = stamina.get_saturation(player) + change
	level = math.max(level, 0)
	level = math.min(level, settings.visual_max)
	stamina.update_saturation(player, level)
	return true
end

stamina.change = stamina.change_saturation -- for backwards compatablity
--- END SATURATION API ---
--- POISON API ---
function stamina.is_poisoned(player)
	return get_player_attribute(player, attribute.poisoned) == "yes"
end

function stamina.set_poisoned(player, poisoned)
	local hud_id = get_hud_id(player)
	if poisoned then
		hb.change_hudbar(player, "health", nil, nil,
			"hbhunger_icon_health_poison.png", nil,
			"hbhunger_bar_health_poison.png")
		set_player_attribute(player, attribute.poisoned, "yes")
	else
		hb.change_hudbar(player, "health", nil, nil,
			"hudbars_icon_health.png", nil, "hudbars_bar_health.png")
		set_player_attribute(player, attribute.poisoned, nil)
	end
end

local function poison_tick(player_name, ticks, interval, elapsed)
	local player = minetest.get_player_by_name(player_name)
	if not player or not stamina.is_poisoned(player) then
		return
	elseif elapsed > ticks then
		stamina.set_poisoned(player, false)
	else
		local hp = player:get_hp() - 1
		if hp > 0 then
			player:set_hp(hp, {type = "set_hp", cause = "stamina:poison"})
		end
		minetest.after(interval, poison_tick, player_name, ticks, interval, elapsed + 1)
	end
end

stamina.registered_on_poisons = {}
function stamina.register_on_poison(fun)
	table.insert(stamina.registered_on_poisons, fun)
end

function stamina.poison(player, ticks, interval)
	for _, fun in ipairs(stamina.registered_on_poisons) do
		local rv = fun(player, ticks, interval)
		if rv == true then
			return
		end
	end
	if not is_player(player) then
		return
	end
	stamina.set_poisoned(player, true)
	local player_name = player:get_player_name()
	poison_tick(player_name, ticks, interval, 0)
end
--- END POISON API ---
--- EXHAUSTION API ---
stamina.exhaustion_reasons = {
	dig = "dig",
	heal = "heal",
	move = "move",
	place = "place",
	sprint = "sprint",
	time = "time"
}

function stamina.get_exhaustion(player)
	return tonumber(get_player_attribute(player, attribute.exhaustion))
end

function stamina.set_exhaustion(player, exhaustion)
	set_player_attribute(player, attribute.exhaustion, exhaustion)
end

stamina.registered_on_exhaust_players = {}
function stamina.register_on_exhaust_player(fun)
	table.insert(stamina.registered_on_exhaust_players, fun)
end

function stamina.exhaust_player(player, change, cause)
	for _, callback in ipairs(stamina.registered_on_exhaust_players) do
		local result = callback(player, change, cause)
		if result then
			return result
		end
	end

	if not is_player(player) then
		return
	end

	local exhaustion = stamina.get_exhaustion(player) or 0

	exhaustion = exhaustion + change

	if exhaustion >= settings.exhaust_lvl then
		exhaustion = exhaustion - settings.exhaust_lvl
		stamina.change_saturation(player, -1)
	end

	stamina.set_exhaustion(player, exhaustion)
end
--- END EXHAUSTION API ---

--- DUMMY SPRINTING API ---
function stamina.register_on_sprinting(fun) end
function set_sprinting(name, sprinting) end
--- END DUMMY SPRINTING API ---

-- Time based stamina functions
local function move_tick()
	for _,player in ipairs(minetest.get_connected_players()) do
		local controls = player:get_player_control()
		local is_moving = controls.up or controls.down or controls.left or controls.right
		local velocity = player:get_velocity()
		velocity.y = 0
		local horizontal_speed = vector.length(velocity)
		local has_velocity = horizontal_speed > 0.05

		-- Check if the player is moving on a boat or other vehicle
		if not has_velocity then
			local attach = player:get_attach()
			if attach then
				local avel = attach:get_velocity()
				avel.y = 0
				has_velocity = vector.length(avel) > 0.05
			end
		end

		if is_moving and has_velocity then
			stamina.exhaust_player(player, settings.exhaust_move, stamina.exhaustion_reasons.move)
		end
	end
end

local function stamina_tick()
	-- raise exhaustion by a little bit every tick
	-- replaces the feature in which saturation decreases after
	-- a long time interval
	for _,player in ipairs(minetest.get_connected_players()) do
		local saturation = stamina.get_saturation(player)
		if saturation > settings.tick_min then
			stamina.exhaust_player(player, settings.exhaust_tick, stamina.exhaustion_reasons.time)
		end
	end
end

local function health_tick(dtime)
	-- YAMS-TODO: optimize
	local gpa = get_player_attribute  -- shorthand

	-- heal or damage player, depending on saturation
	for _,player in ipairs(minetest.get_connected_players()) do
		local air = player:get_breath() or 0
		local hp = player:get_hp()
		local hp_max = player:get_properties().hp_max
		local saturation = stamina.get_saturation(player)
		local heal_cd = tonumber(gpa(player, attribute.heal_cooldown)) or
		                settings.health_tick
		local starving_cd = tonumber(gpa(player, attribute.starving_cooldown)) or
		                    settings.health_tick
		local starve_buildup = tonumber(gpa(player, attribute.starve_buildup)) or 0

		-- don't heal if dead, drowning, or poisoned
		local should_heal = (
			saturation >= settings.heal_lvl and
			hp < hp_max and
			hp > 0 and
			air > 0
			and not stamina.is_poisoned(player)
		)
		-- or damage player by 1 hp if saturation is < 2 (of 30)
		local is_starving = (
			saturation < settings.starve_lvl and
			hp > 0
		)

		if is_starving then
			starving_cd = starving_cd - dtime
			heal_cd = settings.health_tick
		elseif should_heal then
			heal_cd = heal_cd - dtime
		end

		if not is_starving and hp > 0 then
			starving_cd = settings.health_tick
			starve_buildup = 0
		end

		if not should_heal and hp > 0 then
			heal_cd = settings.health_tick
		end

		if heal_cd <= 0 then
			heal_cd = heal_cd + settings.health_tick

			-- try to heal 10% HP every tick (1/10) and carry the remainder
			-- over for the next tick until HP is full
			local raw_amount = hp_max / 10
			local carryover = tonumber(gpa(player, attribute.heal_carryover)) or 0

			-- HP should always be an integer
			local actual_amount = math.floor(raw_amount + carryover)
			carryover = raw_amount - actual_amount
			set_player_attribute(player, attribute.heal_carryover, carryover)

			local new_hp = hp + (actual_amount * settings.heal)
			if new_hp > hp_max then
				new_hp = hp_max
				set_player_attribute(player, attribute.heal_carryover, 0)
			end

			player:set_hp(new_hp, {type = "set_hp", cause = "stamina:heal"})
			stamina.exhaust_player(player, settings.exhaust_lvl, stamina.exhaustion_reasons.heal)
		elseif is_starving and starving_cd <= 0 then
			starving_cd = starving_cd + settings.health_tick
			starve_buildup = starve_buildup + 1
			player:set_hp(hp - starve_buildup, {type = "set_hp", cause = "stamina:starve"})
			set_player_attribute(player, attribute.heal_carryover, 0)
		end

		set_player_attribute(player, attribute.heal_cooldown, heal_cd)
		set_player_attribute(player, attribute.starving_cooldown, starving_cd)
		set_player_attribute(player, attribute.starve_buildup, starve_buildup)
	end
end

local stamina_timer = 0
local health_timer = 0
local action_timer = 0

local function stamina_globaltimer(dtime)
	stamina_timer = stamina_timer + dtime
	health_timer = health_timer + dtime
	action_timer = action_timer + dtime

	if action_timer > settings.move_tick then
		action_timer = action_timer - settings.move_tick
		move_tick()
	end

	if stamina_timer > settings.stamina_tick then
		stamina_timer = stamina_timer - settings.stamina_tick
		stamina_tick()
	end

	-- handled differently because each player has their own timers now
	if health_timer > 0.5 then
		health_timer = health_timer - 0.5
		health_tick(0.5)  -- half a second is precise enough
	end
end

local function reset_heal_cooldown(player, hp_change, reason)
	if hp_change < 0 then  -- Negative values indicate damage
		set_player_attribute(player, attribute.heal_cooldown, settings.health_tick)
		set_player_attribute(player, attribute.heal_carryover, 0)
	end
end

minetest.register_on_player_hpchange(reset_heal_cooldown, false)

local function show_eat_particles(player, itemname)
	-- particle effect when eating
	local pos = player:get_pos()
	pos.y = pos.y + (player:get_properties().eye_height * .923) -- assume mouth is slightly below eye_height
	local dir = player:get_look_dir()

	local def = minetest.registered_items[itemname]
	local texture = def.inventory_image or def.wield_image

	local particle_def = {
		amount = 5,
		time = 0.1,
		minpos = pos,
		maxpos = pos,
		minvel = {x = dir.x - 1, y = dir.y, z = dir.z - 1},
		maxvel = {x = dir.x + 1, y = dir.y, z = dir.z + 1},
		minacc = {x = 0, y = -5, z = 0},
		maxacc = {x = 0, y = -9, z = 0},
		minexptime = 1,
		maxexptime = 1,
		minsize = 1,
		maxsize = 2,
	}

	if texture and texture ~= "" then
		particle_def.texture = texture

	elseif def.type == "node" then
		particle_def.node = {name = itemname, param2 = 0}

	else
		particle_def.texture = "blank.png"
	end

	minetest.add_particlespawner(particle_def)
end

-- override minetest.do_item_eat() so we can redirect hp_change to stamina
stamina.core_item_eat = minetest.do_item_eat
function minetest.do_item_eat(hp_change, replace_with_item, itemstack, player, pointed_thing)
	for _, callback in ipairs(minetest.registered_on_item_eats) do
		local result = callback(hp_change, replace_with_item, itemstack, player, pointed_thing)
		if result then
			return result
		end
	end

	if not is_player(player) or not itemstack then
		return itemstack
	end

	-- Allow players to eat food anyway if sneaking
	local force_eat = player:get_player_control().sneak
	local level = stamina.get_saturation(player) or 0
	if level >= settings.visual_max and hp_change > 0 and not force_eat then
		-- don't eat if player is full and item provides saturation
		local msg = S("[yams] You are full.") .. " " ..
		            S("Sneak while eating if you really want to eat it.")
		minetest.chat_send_player(player:get_player_name(), msg)

		return itemstack
	end

	local itemname = itemstack:get_name()
	if replace_with_item then
		stamina.log("action", "%s eats %s for %s stamina, replace with %s",
			player:get_player_name(), itemname, hp_change, replace_with_item)
	else
		stamina.log("action", "%s eats %s for %s stamina",
			player:get_player_name(), itemname, hp_change)
	end
	minetest.sound_play("stamina_eat", {to_player = player:get_player_name(), gain = 0.7}, true)

	if hp_change > 0 then
		stamina.change_saturation(player, hp_change)
		stamina.set_exhaustion(player, 0)
	elseif hp_change < 0 then
		stamina.poison(player, -hp_change, settings.poison_tick)
	end

	if settings.eat_particles then
		show_eat_particles(player, itemname)
	end

	itemstack:take_item()
	player:set_wielded_item(itemstack)
	replace_with_item = ItemStack(replace_with_item)
	if not replace_with_item:is_empty() then
		local inv = player:get_inventory()
		replace_with_item = inv:add_item("main", replace_with_item)
		if not replace_with_item:is_empty() then
			local pos = player:get_pos()
			pos.y = math.floor(pos.y)
			minetest.add_item(pos, replace_with_item)
		end
	end

	return nil -- don't overwrite wield item a second time
end

minetest.register_on_joinplayer(function(player)
	local level = stamina.get_saturation(player) or settings.initial_saturation

	if minetest.get_modpath("hudbars") then
		hb.init_hudbar(player, "hunger", level, settings.visual_max, false)
		if level == 0 then
			hb.change_hudbar(player, "hunger", nil, nil, nil, nil, nil, nil, 0xFF0000)
		elseif level < settings.heal_lvl then
			hb.change_hudbar(player, "hunger", nil, nil, nil, nil, nil, nil, 0xFF4500)
		end
	end

	stamina.set_saturation(player, level)
	-- reset poisoned
	stamina.set_poisoned(player, false)
	-- remove legacy hud_id from player metadata
	set_player_attribute(player, "stamina:hud_id", nil)
end)

minetest.register_on_leaveplayer(function(player)
	-- nothing?
end)

minetest.register_globalstep(stamina_globaltimer)

minetest.register_on_placenode(function(pos, oldnode, player, ext)
	stamina.exhaust_player(player, settings.exhaust_place, stamina.exhaustion_reasons.place)
end)
minetest.register_on_dignode(function(pos, oldnode, player, ext)
	stamina.exhaust_player(player, settings.exhaust_dig, stamina.exhaustion_reasons.dig)
end)
minetest.register_on_respawnplayer(function(player)
	stamina.update_saturation(player, settings.initial_saturation)
end)

if minetest.get_modpath("hudbars") then
	hb.register_hudbar("hunger", 0xFFFFFF, S("Satiation"),
		{bar = "hbhunger_bar.png",
		 icon = "hbhunger_icon.png",
		 bgicon = "hbhunger_bgicon.png"},
		0, settings.visual_max, false
	)
else
	minetest.log("warning", "hudbars is missing!")
end
