--------------------------------------------------------
-- Minetest :: Pride Flags Mod (pride_flags)
--
-- See README.txt for licensing and other information.
-- Copyright (c) 2022, Leslie E. Krause and Wuzzy
--------------------------------------------------------

pride_flags = {}

local wind_noise = PerlinNoise( 204, 1, 0, 500 )
-- Check whether the new `get_2d` Perlin function is available,
-- otherwise use `get2d`. Needed to suppress deprecation
-- warning in newer Minetest versions.
local old_get2d = true
if wind_noise.get_2d then
	old_get2d = false
end
local active_flags = { }

local pi = math.pi
local rad_180 = pi
local rad_90 = pi / 2

-- This flag is used as the default or fallback in case of error/missing data
local DEFAULT_FLAG = "rainbow"

-- Flag list for the old number-based storing of flags, used up to
-- 8fd4f9661e123bc84c0499c4809537e8aeb24c3b.
-- DO NOT CHANGE THIS LIST!
local legacy_flag_list = {
	"rainbow", "lesbian", "bisexual", "transgender", "genderqueer", "nonbinary", "pansexual", "asexual",
	"vincian", "polysexual", "omnisexual", "graysexual", "demisexual", "homoromantic", "biromantic",
	"polyromantic", "panromantic", "omniromantic", "aromantic", "grayromantic", "demiromantic",
	"androgyne", "demigender", "maverique", "neutrois", "multigender", "polygender", "pangender", "agender",
	"genderfluid", "intersex", "polyamorous", "queer", "demigirl", "demiboy", "bigender", "trigender",
}
local flag_list = {
	-- broader community
	"rainbow", -- rainbow flag / LGBTQ+ Pride flag / Gay Pride flag
	"progress", -- Progress Pride
	-- orientations (general)
	"lesbian", "vincian",
	-- sexual orientations
	"bisexual", "pansexual", "polysexual", "omnisexual",  -- m-spec
	"asexual", "graysexual", "demisexual", -- a-spec
	-- romantic orientations
	"homoromantic",
	"biromantic", "polyromantic", "panromantic", "omniromantic", -- m-spec
	"aromantic", "grayromantic", "demiromantic", -- a-spec
	-- gender-related
	---- umbrella terms
	"transgender", "genderqueer", "nonbinary",
	---- identities that refer to 0 or multiple genders
	"multigender", "polygender", "pangender",
	"agender", "bigender", "trigender",
	---- identities that refer to a specific gender
	"demigender", "demigirl", "demiboy",
	"androgyne", "maverique", "neutrois",
	---- genderfluid
	"genderfluid",
	-- sex-related
	"intersex",
	-- relationship
	"polyamorous",
	-- queer
	"queer",}

local next_flag, prev_flag
local update_next_prev_flag_lists = function()
	next_flag = {}
	prev_flag = {}
	for f=1, #flag_list do
		local name1 = flag_list[f]
		local name0, name2
		if f < #flag_list then
			name2 = flag_list[f+1]
		else
			name2 = flag_list[1]
		end
		if f == 1 then
			name0 = flag_list[#flag_list]
		else
			name0 = flag_list[f-1]
		end
		next_flag[name1] = name2
		prev_flag[name1] = name0
	end
end
update_next_prev_flag_lists()

local flag_exists = function(flag_name)
	return next_flag[ flag_name] ~= nil
end
local get_next_flag = function(current_flag_name)
	if not current_flag_name or not flag_exists( current_flag_name ) then
		return DEFAULT_FLAG
	else
		return next_flag[current_flag_name]
	end
end
local get_prev_flag = function(current_flag_name)
	if not current_flag_name or not flag_exists( current_flag_name ) then
		return DEFAULT_FLAG
	else
		return prev_flag[current_flag_name]
	end
end

local S
if minetest.get_translator then
	S = minetest.get_translator("pride_flags")
else
	S = function(s) return s end
end

-- Delete entity if there is no flag mast node
local delete_if_orphan = function( self )
	local pos = self.object:get_pos( )
	local node = minetest.get_node({x=pos.x,y=pos.y-1,z=pos.z})
	if node.name ~= "pride_flags:upper_mast" and node.name ~= "ignore" then
		minetest.log("action", "[pride_flags] Orphan flag entity removed at "..minetest.pos_to_string(pos, 1))
		self.object:remove( )
		return true
	end
	return false
end

minetest.register_entity( "pride_flags:wavingflag", {
	initial_properties = {
		physical = false,
		visual = "mesh",
		visual_size = { x = 8.5, y = 8.5 },
		collisionbox = { 0, 0, 0, 0, 0, 0 },
		backface_culling = false,
		pointable = false,
		mesh = "pride_flags_wavingflag.b3d",
		textures = { "prideflag_rainbow.png" },
		use_texture_alpha = false,
	},

	on_activate = function ( self, staticdata, dtime )
		if delete_if_orphan( self) then
			return
		end
		-- Init stuff
		self:reset_animation( true )
		self.object:set_armor_groups( { immortal = 1 } )

		if staticdata ~= "" then
			local data = minetest.deserialize( staticdata )
			if data.flag_name then
				self.flag_name = data.flag_name
			else
				-- Convert legacy flag number to flag name
				local flag_idx = data.flag_idx
				self.flag_name = legacy_flag_list[ flag_idx ]
				if not self.flag_name then
					self.flag_name = DEFAULT_FLAG
				end
			end
			self.node_idx = data.node_idx

			if not self.flag_name or not self.node_idx then
				self.object:remove( )
				return
			end

			self:reset_texture( self.flag_name )

			active_flags[ self.node_idx ] = self.object
		else
			self.flag_name = DEFAULT_FLAG
		end

		-- Delete entity if there is already one for this pos
		local objs = minetest.get_objects_inside_radius( self.object:get_pos(), 0.5 )
		for o=1, #objs do
			local obj = objs[o]
			local lua = obj:get_luaentity( )
			if lua and self ~= lua and lua.name == "pride_flags:wavingflag" then
				if lua.node_idx == self.node_idx then
					self.object:remove( )
					return
				end
			end
		end
	end,

	on_deactivate = function ( self )
		if self.sound_id then
			minetest.sound_stop( self.sound_id )
		end
	end,

	on_step = function ( self, dtime )
		self.anim_timer = self.anim_timer - dtime

		if self.anim_timer <= 0 then
			if delete_if_orphan( self) then
				return
			end
			self:reset_animation( )
		end
	end,

	reset_animation = function ( self, initial )
		local pos = self.object:get_pos( )
		local cur_wind = pride_flags.get_wind( pos )
		minetest.log("verbose", "[pride_flags] Current wind at "..minetest.pos_to_string(pos, 1)..": " .. cur_wind)
		local anim_speed
		local wave_sound

		if cur_wind < 10 then
			anim_speed = 10	-- 2 cycle
			wave_sound = "pride_flags_flagwave1"
		elseif cur_wind < 20 then
			anim_speed = 20  -- 4 cycles
			wave_sound = "pride_flags_flagwave1"
		elseif cur_wind < 40 then
			anim_speed = 40  -- 8 cycles
			wave_sound = "pride_flags_flagwave2"
		else
			anim_speed = 80  -- 16 cycles
			wave_sound = "pride_flags_flagwave3"
		end
		-- slightly anim_speed change to desyncronize flag waving
		anim_speed = anim_speed + math.random(0, 200) * 0.01

		if self.object then
			if initial or (not self.object.set_animation_frame_speed) then
				self.object:set_animation( { x = 1, y = 575 }, anim_speed, 0, true )
			else
				self.object:set_animation_frame_speed(anim_speed)
			end
		end
		if not self.sound_id then
			self.sound_id = minetest.sound_play( wave_sound, { object = self.object, gain = 1.0, loop = true } )
		end

		self.anim_timer = 115 + math.random(-10, 10) -- time to reset animation
	end,

	reset_texture = function ( self, flag_name, nextprev )
		if nextprev == 1 then
			self.flag_name = get_next_flag(self.flag_name)
		elseif nextprev == -1 then
			self.flag_name = get_prev_flag(self.flag_name)
		else
			self.flag_name = flag_name
		end

		local texture = string.format( "prideflag_%s.png", self.flag_name )
		self.object:set_properties( { textures = { texture } } )
		return self.flag_name
	end,

	get_staticdata = function ( self )
		return minetest.serialize( {
			node_idx = self.node_idx,
			flag_name = self.flag_name,
		} )
	end,
} )

local metal_sounds
if minetest.get_modpath("default") ~= nil then
	if default.node_sound_metal_defaults then
		metal_sounds = default.node_sound_metal_defaults()
	end
end

minetest.register_node( "pride_flags:lower_mast", {
        description = S("Flag Pole"),
        drawtype = "mesh",
        paramtype = "light",
        mesh = "pride_flags_mast_lower.obj",
        paramtype2 = "facedir",
        tiles = { "pride_flags_baremetal.png", "pride_flags_baremetal.png" },
        wield_image = "pride_flags_pole_bottom_inv.png",
        inventory_image = "pride_flags_pole_bottom_inv.png",
        groups = { cracky = 2 },
        sounds = metal_sounds,
	is_ground_content = false,

        selection_box = {
                type = "fixed",
                fixed = { { -3/32, -1/2, -3/32, 3/32, 1/2, 3/32 } },
        },
        collision_box = {
                type = "fixed",
                fixed = { { -3/32, -1/2, -3/32, 3/32, 1/2, 3/32 } },
        },

	on_rotate = function(pos, node, user, mode, new_param2)
		if mode == screwdriver.ROTATE_AXIS then
			return false
		end
	end,
} )

local function get_flag_pos( pos, param2 )
	local facedir_to_pos = {
		[0] = { x = 0, y = 0.6, z = -0.1 },
		[1] = { x = -0.1, y = 0.6, z = 0 },
		[2] = { x = 0, y = 0.6, z = 0.1 },
		[3] = { x = 0.1, y = 0.6, z = 0 },
	}
	return vector.add( pos, vector.multiply( facedir_to_pos[ param2 ], 1 ) )
end

local function rotate_flag_by_param2( flag, param2 )
	local facedir_to_yaw = {
		[0] = rad_90,
		[1] = 0,
		[2] = -rad_90,
		[3] = rad_180,
	}
	local baseyaw = facedir_to_yaw[ param2 ]
	if not baseyaw then
		minetest.log("warning", "[pride_flags] Unsupported flag pole node param2: "..tostring(param2))
		return
	end
	flag:set_yaw( baseyaw - rad_180 )
end

local function spawn_flag( pos )
	local node_idx = minetest.hash_node_position( pos )
	local param2 = minetest.get_node( pos ).param2

	local flag_pos = get_flag_pos( pos, param2 )
	local obj = minetest.add_entity( flag_pos, "pride_flags:wavingflag" )
	if not obj or not obj:get_luaentity( ) then
		return
	end

	obj:get_luaentity( ).node_idx = node_idx
	rotate_flag_by_param2( obj, param2 )

	active_flags[ node_idx ] = obj
	return obj
end

local function spawn_flag_and_set_texture( pos )
	local flag = spawn_flag( pos )
	if flag and flag:get_luaentity() then
		local meta = minetest.get_meta( pos )
		local flag_name = meta:get_string("flag_name")
		if flag_name == "" then
			-- Convert legacy flag number to flag name
			local flag_idx = meta:get_int("flag_idx")
			flag_name = legacy_flag_list[flag_idx]
			if not flag_name then
				flag_name = DEFAULT_FLAG
			end
			meta:set_string("flag_idx", "")
			meta:set_string("flag_name", flag_name)
		end
		flag:get_luaentity():reset_texture( flag_name )
	end
	return flag
end

local function cycle_flag( pos, player, cycle_backwards )
	local pname = player:get_player_name( )
	if minetest.is_protected( pos, pname ) and not
			minetest.check_player_privs( pname, "protection_bypass") then
		minetest.register_protection_violation( pos, pname )
		return
	end

	local node_idx = minetest.hash_node_position( pos )

	local aflag = active_flags[ node_idx ]
	local flag
	if aflag then
		flag = aflag:get_luaentity( )
	end
	if flag then
		local flag_name
		if cycle_backwards then
			flag_name = flag:reset_texture( nil, -1 )
		else
			flag_name = flag:reset_texture( nil, 1 )
		end
		local meta = minetest.get_meta( pos )
		meta:set_string("flag_name", flag_name)
	else
		spawn_flag_and_set_texture( pos )
	end
end

minetest.register_node( "pride_flags:upper_mast", {
	description = S("Flag Pole with Flag"),
	drawtype = "mesh",
	paramtype = "light",
	mesh = "pride_flags_mast_upper.obj",
	paramtype2 = "facedir",
	tiles = { "pride_flags_baremetal.png", "pride_flags_baremetal.png" },
	wield_image = "pride_flags_pole_top_inv.png",
	inventory_image = "pride_flags_pole_top_inv.png",
	groups = { cracky = 2 },
	sounds = metal_sounds,
	is_ground_content = false,

        selection_box = {
                type = "fixed",
                fixed = { { -3/32, -1/2, -3/32, 3/32, 27/16, 3/32 } },
        },
        collision_box = {
                type = "fixed",
                fixed = { { -3/32, -1/2, -3/32, 3/32, 1/2, 3/32 } },
        },

	on_rightclick = function ( pos, node, player )
		cycle_flag( pos, player )
	end,

	on_punch = function ( pos, node, player )
		cycle_flag( pos, player, -1 )
	end,

	node_placement_prediction = "",

	on_place = function( itemstack, placer, pointed_thing )
		if not pointed_thing.type == "node" then
			return itemstack
		end

		local node = minetest.get_node(pointed_thing.under)
		local pdef = minetest.registered_nodes[node.name]
		if pdef and pdef.on_rightclick and
				not placer:get_player_control().sneak then
			return pdef.on_rightclick(pointed_thing.under,
					node, placer, itemstack, pointed_thing)
		end

		local pos
		if pdef and pdef.buildable_to then
			pos = pointed_thing.under
		else
			pos = pointed_thing.above
			node = minetest.get_node(pos)
			pdef = minetest.registered_nodes[node.name]
			if not pdef or not pdef.buildable_to then
				return itemstack
			end
		end

		local above1 = {x = pos.x, y = pos.y + 1, z = pos.z}
		local above2 = {x = pos.x, y = pos.y + 2, z = pos.z}
		local anode1 = minetest.get_node_or_nil(above1)
		local anode2 = minetest.get_node_or_nil(above2)
		local adef1 = anode1 and minetest.registered_nodes[anode1.name]
		local adef2 = anode2 and minetest.registered_nodes[anode2.name]

		-- Don't build if upper nodes are blocked, unless it's a hidden node
		if not adef1 or (not adef1.buildable_to and anode1.name ~= "pride_flags:upper_mast_hidden_1") then
			return itemstack
		end
		if not adef2 or (not adef2.buildable_to and anode2.name ~= "pride_flags:upper_mast_hidden_2") then
			return itemstack
		end

		local pn = placer:get_player_name()
		if minetest.is_protected(pos, pn) then
			minetest.record_protection_violation(pos, pn)
			return itemstack
		elseif minetest.is_protected(above1, pn) then
			minetest.record_protection_violation(above1, pn)
			return itemstack
		elseif minetest.is_protected(above2, pn) then
			minetest.record_protection_violation(above2, pn)
			return itemstack
		end

		local yaw = placer:get_look_horizontal( )
		local dir = minetest.yaw_to_dir( yaw )
		local param2 = (minetest.dir_to_facedir( dir ) + 3) % 4
		minetest.set_node(pos, {name = "pride_flags:upper_mast", param2 = param2 })
		minetest.set_node(above1, {name = "pride_flags:upper_mast_hidden_1"})
		minetest.set_node(above2, {name = "pride_flags:upper_mast_hidden_2"})

		if not (minetest.is_creative_enabled(pn)) then
			itemstack:take_item()
		end

		local def = minetest.registered_nodes["pride_flags:upper_mast"]
		if def and def.sounds then
			minetest.sound_play(def.sounds.place, {pos = pos}, true)
		end

		return itemstack
	end,

	on_construct = function ( pos )
		local flag = spawn_flag ( pos )
		if flag and flag:get_luaentity() then
			local meta = minetest.get_meta( pos )
			meta:set_string("flag_name", flag:get_luaentity().flag_name)
		end
	end,

	on_destruct = function ( pos )
		local node_idx = minetest.hash_node_position( pos )
		if active_flags[ node_idx ] then
			active_flags[ node_idx ]:remove( )
		end
	end,

	after_destruct = function( pos )
		local above1 = {x=pos.x, y=pos.y+1, z=pos.z}
		local above2 = {x=pos.x, y=pos.y+2, z=pos.z}
		if minetest.get_node( above1 ).name == "pride_flags:upper_mast_hidden_1" then
			minetest.remove_node( above1 )
		end
		if minetest.get_node( above2 ).name == "pride_flags:upper_mast_hidden_2" then
			minetest.remove_node( above2 )
		end
	end,

	on_rotate = function(pos, node, user, mode, new_param2)
		if mode == screwdriver.ROTATE_AXIS then
			return false
		end
		local node_idx = minetest.hash_node_position( pos )
		local aflag = active_flags[ node_idx ]
		if aflag then
			local lua = aflag:get_luaentity( )
			if not lua then
				aflag = spawn_flag_and_set_texture( pos )
				if aflag then
					lua = aflag:get_luaentity()
					if not lua then
						 return
					 end
				 end
			end
			local flag_pos_idx = lua.node_idx
			local flag_pos = minetest.get_position_from_hash( flag_pos_idx )
			flag_pos = get_flag_pos( flag_pos, new_param2 )
			rotate_flag_by_param2( aflag, new_param2 )
			aflag:set_pos( flag_pos )
		end
	end,
} )

minetest.register_lbm({
	name = "pride_flags:respawn_flags",
	label = "Respawn flags",
	nodenames = {"pride_flags:upper_mast"},
	run_at_every_load = true,
	action = function(pos, node)
		local node_idx = minetest.hash_node_position( pos )
		local aflag = active_flags[ node_idx ]
		if aflag then
			return
		end
		spawn_flag_and_set_texture( pos )
	end
})

minetest.register_lbm({
	name = "pride_flags:update_node_meta",
	label = "Update mast node meta",
	nodenames = {"pride_flags:upper_mast"},
	run_at_every_load = false,
	action = function(pos, node)
		local meta = minetest.get_meta( pos )
		local flag_idx = meta:get_int("flag_idx")
		if flag_idx ~= 0 then
			local flag_name = legacy_flag_list[ flag_idx ]
			if not flag_name then
				flag_name = DEFAULT_FLAG
			end
			meta:set_string("flag_idx", "")
			meta:set_string("flag_name", flag_name)
		end
	end
})

if minetest.get_modpath("default") then
	minetest.register_craft({
		output = "pride_flags:lower_mast 6",
		recipe = {
			{"default:steel_ingot"},
			{"default:steel_ingot"},
			{"default:steel_ingot"},
		},
	})
end
minetest.register_craft({
	output = "pride_flags:upper_mast",
	recipe = {
		{"pride_flags:lower_mast", "group:wool", "group:wool"},
		{"pride_flags:lower_mast", "group:wool", "group:wool"},
	},
})

-- Add 2 hidden upper mast segments to block all the nodes
-- the upper mast occupies. This is also needed to prevent
-- collision issues with overhigh nodes.
-- These nodes will be automatically
-- added/removed when the upper mast is constructed
-- or destructed.
minetest.register_node( "pride_flags:upper_mast_hidden_1", {
	drawtype = "airlike",
	pointable = false,
	paramtype = "light",
	sunlight_propagates = true,
	wield_image = "pride_flags_pole_hidden1_inv.png",
	inventory_image = "pride_flags_pole_hidden1_inv.png",
	groups = { not_in_creative_inventory = 1 },
	sounds = metal_sounds,
        collision_box = {
                type = "fixed",
                fixed = { { -3/32, -1/2, -3/32, 3/32, 1/5, 3/32 } },
        },
	is_ground_content = false,
	on_blast = function() end,
	can_dig = function() return false end,
	drop = "",
	diggable = false,
	floodable = false,
})
minetest.register_node( "pride_flags:upper_mast_hidden_2", {
	drawtype = "airlike",
	pointable = false,
	paramtype = "light",
	sunlight_propagates = true,
	wield_image = "pride_flags_pole_hidden2_inv.png",
	inventory_image = "pride_flags_pole_hidden2_inv.png",
	groups = { not_in_creative_inventory = 1 },
	sounds = metal_sounds,
        collision_box = {
                type = "fixed",
                fixed = { { -3/32, -1/2, -3/32, 3/32, -5/16, 3/32 } },
        },
	is_ground_content = false,

	on_blast = function() end,
	can_dig = function() return false end,
	drop = "",
	diggable = false,
	floodable = false,
})

-- API
pride_flags.add_flag = function( name )
	if flag_exists( name ) then
		return false
	end
	table.insert( flag_list, name )
	update_next_prev_flag_lists()
	return true
end

pride_flags.get_flags = function( )
	return table.copy( flag_list )
end

pride_flags.set_flag_at = function( pos, flag_name )
	local node = minetest.get_node( pos )
	if node.name ~= "pride_flags:upper_mast" then
		return false
	end
	if not flag_exists( flag_name ) then
		return false
	end
	local node_idx = minetest.hash_node_position( pos )
	local aflag = active_flags[ node_idx ]
	local flag
	if aflag then
		flag = aflag:get_luaentity( )
	end
	if flag then
		local set_flag_name = flag:reset_texture( flag_name )
		if set_flag_name == flag_name then
			local meta = minetest.get_meta( pos )
			meta:set_string("flag_name", flag_name)
			return true
		end
	end
	return false
end

pride_flags.get_flag_at = function( pos )
	local node = minetest.get_node( pos )
	if node.name ~= "pride_flags:upper_mast" then
		return nil
	end
	local meta = minetest.get_meta( pos )
	local flag_name = meta:get_string("flag_name")
	if flag_name ~= "" then
		return flag_name
	end
end

-- Returns the wind strength at pos.
-- Can be overwritten by mods.
pride_flags.get_wind = function( pos )
	-- The default wind function ignores pos.
	-- Returns a wind between ca. 0 and 55
	local coords = { x = os.time( ) % 65535, y = 0 }
	local cur_wind
	if old_get2d then
		cur_wind = wind_noise:get2d(coords)
	else
		cur_wind = wind_noise:get_2d(coords)
	end
	cur_wind = cur_wind * 30 + 30
	return cur_wind
end

