--[[ init.lua
Copyright 2025 Pixelo789

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

SPDX-License-Identifier: AGPL-3.0-or-later
--]]


thinkingwitharrows = {difficulty = 0}
local modname = core.get_current_modname()
local S = core.get_translator(modname)
local modpath = core.get_modpath(modname)

local dialogue_enabled = core.settings:get_bool("thinkingwitharrows_dialogue_enabled")


-- Helper functions

-- Floor division
function thinkingwitharrows.fd(num, div)
	return math.floor(num / div)
end

-- Nicely format time in seconds
function thinkingwitharrows.format_time(time)
	local min_sec = os.date("!:%M:%S", time)
	local hrs = thinkingwitharrows.fd(time, 3600)

	local r = table.concat({
		hrs,
		min_sec
	})
	if hrs < 10 then
		r = "0" .. r
	end

	return r
end

-- Ticks the given int by 1 in the metadata
function thinkingwitharrows.tick_int(meta, item)
	meta:set_int(item, meta:get_int(item) + 1)
end

-- Return the largest value from a set table
function thinkingwitharrows.largest(t)
	local largest
	for _, v in ipairs(t) do
		if largest == nil then
			largest = v
		elseif largest < v then
			largest = v
		end
	end
	return largest
end


-- Helper function that returns -1 if the first value is larger, else 1
local function largest_weird(num1, num2)
	if num1 > num2 then
		return -1
	end
	return 1
end

-- Helper function to get all nodes between two positions
function thinkingwitharrows.node_area(pos1, pos2)
	local x_n = largest_weird(pos1.x, pos2.x)
	local y_n = largest_weird(pos1.y, pos2.y)
	local z_n = largest_weird(pos1.z, pos2.z)

	local nodes = {}
	for x = pos1.x, pos2.x, x_n do
		for y = pos1.y, pos2.y, y_n do
			for z = pos1.z, pos2.z, z_n do
				table.insert(nodes, {x = x, y = y, z = z})
			end
		end
	end

	return nodes
end

-- Set the node in range between two positions
function thinkingwitharrows.set_node_area(pos1, pos2, node)
	local node = node or {name = "air"}

	core.bulk_set_node(thinkingwitharrows.node_area(pos1, pos2), node)
end

-- Set the node in a range between multiple areas
function thinkingwitharrows.set_node_area_multiple(areas, node)
	local node = node or {name = "air"}

	local nodes = {}
	for _, v in ipairs(areas) do
		local new_nodes = thinkingwitharrows.node_area(v[1], v[2])
		for _, v2 in ipairs(new_nodes) do
			table.insert(nodes, v2)
		end
	end

	core.bulk_set_node(nodes, node)
end


-- Helper function to reset the XBows HUD
function thinkingwitharrows.update_hud(player)
	local inv = player:get_inventory()
	if not inv:is_empty('x_bows:arrow_inv') then
		XBowsQuiver:udate_or_create_hud(player, inv:get_list("x_bows:arrow_inv"))
	else
		XBowsQuiver:udate_or_create_hud(player, {
			ItemStack({name = "x_bows:no_ammo"})
		})
	end
end


-- Level metadata
dofile(core.get_modpath(modname) .. "/levels.lua")
local levels = thinkingwitharrows.get_levels()


-- Get the metadata for the current level
function thinkingwitharrows.get_level_info(player)
	local meta = player:get_meta()
	local level = meta:get_int("level")
	local section = meta:get_int("section")
	return levels[level], level, section
end


-- Helper function to change level
function thinkingwitharrows.change_level_secondary(player, num)
	local name = player:get_player_name()
	local meta = player:get_meta()
	local inv = player:get_inventory()

	local level_count = #levels

	if num < 1 or num > level_count then
		core.chat_send_player(name, S("Level must be in between 1 and @1", level_count))
		return
	end

	meta:set_int("level", num)
	meta:set_int("section", 1)
	meta:set_int("time", 0)
	meta:set_int("level21_destruct_time", 0)

	local level = num
	local section = 1

	thinkingwitharrows.set_level(num)

	if levels[level].sections[section].clear_inventory then
		inv:set_list("main", {})
	else
		thinkingwitharrows.remove_mineable(inv)
	end

	player:set_pos(levels[num].sections[1].pos)
	thinkingwitharrows_antigravity.set_gravity_earthmoon(player, levels[num].moon_gravity)

	thinkingwitharrows.stop_self_destruct()
end


-- Function to set the inventory formspec
function thinkingwitharrows.set_inventory_formspec(player)
	local meta = player:get_meta()

	local level = meta:get_int("level")
	local level_str = string.format(levels[level].fake_levels, level)
	local clicks = meta:get_int("clicks")
	local blacks = meta:get_int("blacks")
	local blues = meta:get_int("blues")
	local reds = meta:get_int("reds")
	local bullsyes = meta:get_int("bullsyes")

	-- ratios for bar graph
	local largest = thinkingwitharrows.largest {clicks, blacks, blues, reds, bullsyes}
	local r_clicks = clicks / largest
	local r_blacks = blacks / largest
	local r_blues = blues / largest
	local r_reds = reds / largest
	local r_bullsyes = bullsyes / largest
	local ratio_table = {
		{ratio = r_clicks, color = "#000000FF", label = S("Clicks")},
		{ratio = r_blacks, color = "#000000FF", label = S("Blacks")},
		{ratio = r_blues, color = "#639BFFFF", label = S("Blues")},
		{ratio = r_reds, color = "#D95763FF", label = S("Reds")},
		{ratio = r_bullsyes, color = "#FBF236FF", label = S("Bullsyes")},
	}

	local size = 6

	local time = meta:get_int("time")
	local timer_color = "#00FF00FF"
	if time == 0 then
		time = core.get_gametime()
		timer_color = "#FFFFFFFF"
	end

	if meta:get_int("speedrun_has_cheated") ~= 0 then
		timer_color = "#FF0000FF"
	end

	local formspec_table = {
		"formspec_version[4]",
		"size[12, 10]",
		"hypertext[0.375, 0.375;", 10, ",2;;<global margin=0><style color=", timer_color, ">", S("Time: @1", thinkingwitharrows.format_time(time)), "</style>\n", -- time
		S("Level: @1", level_str), "]", -- level counter
		"box[0.375, 2.875; 11.25, ", size + 0.75, ";#555555FF]", -- bar graph background
	}

	-- bar graph bars
	for k, v in ipairs(ratio_table) do
		table.insert(formspec_table, table.concat {
			"box[", -1.7525 + k * 2.3375, ",", 9.055 - (v.ratio * size), ";1.5,", v.ratio * size, ";", v.color,
			"]hypertext[", -1.7525 + k * 2.3375, ",9.205;1.5,1;;<global size=10 halign=center margin=0>", v.label, "]"
		})
	end

	-- reset button
	if level >= 1 then
		table.insert(formspec_table, table.concat {
			"button[6, 0.375;5.625, 0.75;reset_section;", S("Reset Section"), "]"
		})
		formspec_table[4] = 5.5
	end

	player:set_inventory_formspec(table.concat(formspec_table))
end


local function set_schematic(name, pos)
	core.place_schematic(pos, modpath .."/schems/" .. name, "0", nil, true)
end


function thinkingwitharrows.set_level(level)
	set_schematic("level" .. level .. ".mts", {x = 0, y = 0, z = 0})
end


-- Remove all mineable blocks from player inventory
function thinkingwitharrows.remove_mineable(inv)
	inv:remove_item("main", ItemStack("thinkingwitharrows_nodes:mineable 1000"))
	inv:remove_item("main", ItemStack("thinkingwitharrows_bows:mineable_reflector 1000"))
end


function thinkingwitharrows.progress(player, area, distance, target_pos)
	local meta = player:get_meta()

	-- changing level/section
	local level = meta:get_int("level")
	local section = meta:get_int("section")
	local time_delay = 0
	local transitioning = meta:get_int("transitioning")
	if levels[level].sections[section].custom_target then
		levels[level].sections[section].custom_target(player, area, distance, target_pos)
		return
	elseif level == #levels and section == #levels[level].sections then -- generic last level beaten
		set_schematic("final.mts", {x = 0, y = 0, z = 0})
		meta:set_int("time", core.get_gametime())
		meta:set_int("level", -1)
		level = -1
		section = 1
		thinkingwitharrows_antigravity.set_gravity_earthmoon(player, false)
	else
		if levels[level].sections[section].after_target then
			levels[level].sections[section].after_target(player)
		end
		if section == #levels[level].sections then -- new level
			time_delay = 2.5 - 257/341
			if transitioning == 0 then
				thinkingwitharrows_transitions.queue_transition(player)
			end

			core.after(257/341, function()
				thinkingwitharrows.set_level(level + 1)
				meta:set_int("level", level + 1)
				meta:set_int("latest_level", level + 1)
				meta:set_int("section", 1)
				level = level + 1
				section = 1

				-- gravity
				thinkingwitharrows_antigravity.set_gravity_earthmoon(player, levels[level].moon_gravity)

				-- add arrows
				local arrows = levels[level].arrows or 5
				if arrows > 0 then
					thinkingwitharrows_bows.add_arrows(player, arrows)
				end
			end)
		else -- new section
			core.remove_node(target_pos)
			meta:set_int("section", section + 1)
			section = section + 1
		end
	end
	core.after(time_delay, function()
		-- removing mineable blocks from inventory
		local inv = player:get_inventory()
		thinkingwitharrows.remove_mineable(inv)

		-- teleporting
		if transitioning == 0 then
			player:set_pos(levels[level].sections[section].pos)
			player:set_look_horizontal(player:get_look_horizontal() + levels[level].sections[section].rotate)

			local easy_tweaks = levels[level].sections[section].easy_tweaks
			if easy_tweaks and thinkingwitharrows.difficulty == 0 then
				easy_tweaks(player)
			end

			-- target
			if area == 4 then
				thinkingwitharrows.tick_int(meta, "bullsyes")

				if distance >= 15 then
					thinkingwitharrows.tick_int(meta, "far_bullsyes")
				end
			elseif area == 3 then
				thinkingwitharrows.tick_int(meta, "reds")
			elseif area == 2 then
				thinkingwitharrows.tick_int(meta, "blues")
			elseif area == 1 then
				thinkingwitharrows.tick_int(meta, "blacks")
			else
				thinkingwitharrows.tick_int(meta, "clicks")
			end

			local dialogue = levels[level].sections[section].dialogue
			local dialogue_colors = levels[level].sections[section].dialogue_colors
			local after_dialogue = levels[level].sections[section].after_dialogue
			local td = 0.5
			if time_delay > 0 then
				td = 257/341 + 0.5
			end
			if dialogue ~= nil then
				core.after(td, function()
					if dialogue_enable ~= false or after_dialogue ~= nil then
						thinkingwitharrows_dialogue.queue_dialogue(player, dialogue, dialogue_colors, after_dialogue)
					end
				end)
			end
		end
	end)

	thinkingwitharrows_dialogue.remove_dialogue(player)
end


function thinkingwitharrows.restart(player)
	local inv = player:get_inventory()
	inv:set_list("main", {})
	inv:set_list("x_bows:arrow_inv", {})

	-- set player metadata
	local meta = player:get_meta()
	meta:set_int("started", 1)
	meta:set_int("level", 1)
	meta:set_int("latest_level", 1)
	meta:set_int("section", 1)
	meta:set_int("clicks", 0)
	meta:set_int("blacks", 0)
	meta:set_int("blues", 0)
	meta:set_int("reds", 0)
	meta:set_int("bullsyes", 0)
	meta:set_int("far_bullsyes", 0)
	meta:set_int("show_bow_hud", 0)

	-- load level
	thinkingwitharrows.set_level(1)

	-- load transition box
	core.place_schematic({x = -4, y = 0, z = 0}, modpath .."/schems/transition.mts", "0", nil, true)

	-- teleport the player
	player:set_pos(levels[1].sections[1].pos)
	player:set_look_horizontal(0)
	player:set_look_vertical(0)

	-- dialogue
	if dialogue_enabled ~= false then
		core.after(0.5, function()
			thinkingwitharrows_dialogue.queue_dialogue(
				player,
				levels[1].sections[1].dialogue,
				levels[1].sections[1].dialogue_colors
			)
		end)
	end
end


-- Health regeneration function
local function regen_health(player)
	local name = player:get_player_name()
	local hp = player:get_hp()
	if hp < core.PLAYER_MAX_HP_DEFAULT and hp > 0 then
		player:set_hp(hp + 1)
		core.after(0.25, regen_health, player)
	end
end


-- Override the hand tool
local oddly_props = {
	times = {[3] = 0.30},
	uses = 0,
}
local range = 4
if core.settings:get_bool("thinkingwitharrows_editor") == true then
	oddly_props.times[2] = 0.50
	range = 10
end
core.override_item("", {
	wield_image = "wieldhand.png",
	wield_scale = {x = 1, y = 1, z = 2.5},
	tool_capabilities = {
		full_punch_interval = 0.9,
		max_drop_level = 0,
		groupcaps = {
			oddly_breakable_by_hand = oddly_props,
		},
		damage_groups = {fleshy = 1},
	},
	range = range
})


-- This function is called on formspec submission
core.register_on_player_receive_fields(function(player, formname, fields)
	if formname ~= "" then -- if the formspec is not the main inventory then quit
		return
	end

	if fields.reset_section then
		local inv = player:get_inventory()
		local meta = player:get_meta()
		local level = meta:get_int("level")
		local section = meta:get_int("section")

		if meta:get_int("transitioning") == 0 then
			thinkingwitharrows.set_level(level)
			player:set_pos(levels[level].sections[section].pos)
		end

		if levels[level].sections[section].clear_inventory then
			inv:set_list("main", {})
		else
			thinkingwitharrows.remove_mineable(inv)
		end

		if levels[level].sections[section].on_reset_section then
			levels[level].sections[section].on_reset_section(player)
		end

		thinkingwitharrows_antigravity.set_gravity_earthmoon(player, levels[level].moon_gravity)

		local easy_tweaks = levels[level].sections[section].easy_tweaks
		if easy_tweaks and thinkingwitharrows.difficulty == 0 then
			easy_tweaks(player)
		end
	end
end)


-- This function is called when the player joins
core.register_on_joinplayer(function(player, last_login)
	local player_inv = player:get_inventory()
	local player_meta = player:get_meta()

	if player_meta:get_int("started") == 0 then
		thinkingwitharrows.restart(player)
	end

	if player:get_hp() < core.PLAYER_MAX_HP_DEFAULT then
		regen_health(player)
	end

	-- Bloom and volumetric lighting
	player:set_lighting {
		shadows = { intensity = 0.33 },
		bloom = { intensity = 0.05 },
		volumetric_light = { strength = 0.3 },
	}

	player_meta:set_int("transitioning", 0)

	-- Fix in-game lighting
	local level1_min = {x = 0, y = 0, z = 0}
	local level1_max = {x = 26, y = 10, z = 14}
	core.emerge_area(level1_min, level1_max, function(_, _, calls_remaining)
		if calls_remaining == 0 then
			core.fix_light(level1_min, level1_max)
		end
	end)

	core.register_on_player_hpchange(function(player, hp_change, reason)
		if hp_change < 0 then
			core.after(0.5, regen_health, player)
		end
	end)

	-- Show bow HUD if needed
	if player_meta:get_int("show_bow_hud") == 1 then
		thinkingwitharrows.update_hud(player)
	end

	-- HUD flags
	local hud_flags = {
		healthbar = false,
		breathbar = false,
		minimap = false,
		minimap_radar = false,
		basic_debug = false,
	}

	-- Things specific to editor mode
	if core.settings:get_bool("thinkingwitharrows_editor") ~= true then
		player:hud_set_hotbar_itemcount(2)

		-- Every single time the server ticks, update the formspec
		core.register_globalstep(function(dtime)
			thinkingwitharrows.set_inventory_formspec(player)
		end)

		-- When player respawns, teleport them to the start of the section
		core.register_on_respawnplayer(function(player)
			local meta = player:get_meta()
			local level = meta:get_int("level")
			local section = meta:get_int("section")
			player:set_pos(levels[level].sections[section].pos)

			return true -- spawning should not continue with the engine
		end)

		-- Speedrun timer
		local speedrun_mode = core.settings:get_bool("thinkingwitharrows_speedrun_mode")
		if speedrun_mode then
			thinkingwitharrows_speedruntimer.start_timer(player)
			hud_flags.basic_debug = true
		end
	else
		hud_flags.basic_debug = true
		player_meta:set_int("speedrun_has_cheated", 1)
		player:hud_set_hotbar_itemcount(16)
		core.settings:set_bool("creative_mode", true)
		core.settings:set_bool("enable_damage", false)
	end

	-- Things specific to difficulties
	local difficulty = core.settings:get("thinkingwitharrows_difficulty")
	if difficulty == "easy" then
		playerphysics.add_physics_factor(player, "speed", "thinkingwitharrows:difficulty_speedboost", 1.1)
		thinkingwitharrows.difficulty = 0
	elseif difficulty == "hard" then
		thinkingwitharrows.difficulty = 1
	else
		core.log("warning", string.format('Difficulty is configured to an unknown value: "%s"', difficulty))
		thinkingwitharrows.difficulty = 0
	end

	player:hud_set_flags(hud_flags)
end)
