-- Persistent Fullscreen Map Mod for Luanti

persistent_map = {}

-- Configuration
persistent_map.tile_size = 128 -- blocks per tile side
persistent_map.map_image_size = 128 -- pixels per tile image
persistent_map.scan_interval = 0.5 -- seconds between discovery checks
persistent_map.max_height = 80
persistent_map.min_height = -10
persistent_map.gui_tile_size = 5.0 -- formspec size for each tile (much larger)
persistent_map.view_radius = 6 -- tiles in each direction from center (13x13 grid, much bigger)
persistent_map.marker_scale = 7 -- Scale factor for markers (configurable)
persistent_map.player_marker_scale = 4 -- Scale factor for player marker (configurable)
persistent_map.marker_name_scale = 2.0 -- Base scale for marker name text (configurable)
persistent_map.marker_name_font_multiplier = 16 -- Font size multiplier for marker names (configurable)
persistent_map.marker_name_padding = 0.1 -- Padding between marker and name text
persistent_map.marker_name_min_zoom = 0.5 -- Minimum zoom level to show marker names
persistent_map.marker_name_zoom_multiplier = 1.5 -- Multiplier for text scale based on zoom
persistent_map.marker_name_char_width = 0.2 -- Character width estimation for text centering
persistent_map.marker_name_text_height = 0.3 -- Text height for positioning

-- Formspec UI Configuration
persistent_map.ui = {
	button_size = 5,           -- Size of navigation, zoom, and delete buttons
	color_button_size = 5,     -- Size of marker color selection buttons
	side_panel_width = 15.0,      -- Width of left and right control panels
	padding = 1.0,               -- General padding around UI elements
	header_height = 1.0,         -- Height of the header area
	-- Bottom info section configuration
	info_bottom_margin = 4,      -- Distance from bottom of formspec to info section
	info_line_spacing = 2.0,     -- Vertical spacing between info lines
	info_left_align_with_panel = true,  -- Whether to align left info with left panel (true) or use padding (false)
	-- Navigation panel configuration
	nav_panel_offset = 2,        -- Vertical offset for navigation panel from header
	nav_section_spacing = 1,     -- Spacing between navigation sections
	nav_button_spacing = 0.2,    -- Spacing between navigation buttons
	nav_button_multiplier = 1.2, -- Multiplier for west button positioning
	nav_center_button_offset = 1, -- Offset for center button from other nav buttons
	zoom_section_offset = 2,     -- Offset for zoom section from navigation buttons
	zoom_button_spacing = 0.5,   -- Spacing for zoom buttons
	-- Marker panel configuration
	marker_name_label_spacing = 0.8, -- Spacing between "Marker Name:" label and input field
	marker_name_field_offset = 0.5, -- Offset for marker name input field
	marker_name_field_height = 2, -- Height of marker name input field
	marker_colors_offset = 3.5,  -- Offset for marker color buttons from name field
	colors_per_row = 2,          -- Number of color buttons per row
	color_button_spacing = 0.5,  -- Horizontal spacing between color buttons
	color_button_row_spacing = 0.3, -- Vertical spacing between color button rows
	delete_buttons_offset = 1,   -- Offset for delete buttons from color buttons
	delete_button_spacing = 0.3, -- Spacing between delete buttons
	-- Layout configuration
	formspec_version = 6,        -- Formspec version to use
	min_formspec_height = 25,    -- Minimum height for formspec
	header_title_offset = 4,     -- Offset for centering header title
	scroll_content_center = 0.5, -- Centering factor for scroll content
	field_width_offset = 0.5,    -- Width offset for input fields
	button_width_offset = 0.5,   -- Width offset for buttons
	-- Hardcoded value replacements
	half_divisor = 2,            -- Divisor for calculating half values (button_size / 2)
	arrow_height_factor = 0.5,   -- Factor for arrow height calculation in player marker
	total_width_factor = 0.5,    -- Factor for calculating half total width
	padding_multiplier = 2,      -- Multiplier for padding calculations
}

-- Player marker configuration
persistent_map.player_marker = {
	arrow_size = 0.25,           -- Base size of player arrow
	arrow_height = 0.35,         -- Height of player arrow
	main_body_size = 0.2,        -- Size of main body square
	tip_size = 0.14,             -- Size of directional tip
	mid_section_size = 0.16,     -- Size of mid-section indicator
	wing_size = 0.1,             -- Size of wing indicators
	mid_section_factor = 0.5,    -- Factor for mid-section positioning
	wing_factor = 0.6,           -- Factor for wing positioning
}

-- Map marker configuration
persistent_map.map_marker = {
	scale_factor = 0.15,         -- Base scale factor for markers
	half_size_factor = 0.5,      -- Factor for calculating half marker size
	text_center_factor = 0.6,    -- Factor for centering text -(pos offsets text left)
	position_flip_factor = 1.0,  -- Factor for flipping Z position (1.0 = flip, 0.0 = no flip)
}

-- Map generation configuration
persistent_map.generation = {
	default_color_r = 100,       -- Default color red component
	default_color_g = 100,       -- Default color green component
	default_color_b = 100,       -- Default color blue component
	alpha_value = 255,           -- Alpha value for colors
	min_radius = 2,              -- Minimum radius for view calculations
	y_scan_step = -1,            -- Step for Y scanning (negative = top to bottom)
	tile_center_offset = 2,      -- Offset for tile center calculation (tile_size / 2)
	initial_discovery_delay = 0.1, -- Delay for initial tile discovery on player join
}

-- Zoom configuration
-- At max zoom, one tile should fill the screen (map area is ~65 units, base tile is 5, so need ~13x zoom)
persistent_map.zoom_levels = {0.5, 1.0, 2.0, 4.0, 6.0, 8.0, 10.0, 13.0, 16.0, 20.0} -- Available zoom levels
persistent_map.default_zoom_index = 2 -- Default to 1.0x zoom (index 2)
persistent_map.min_zoom_index = 1
persistent_map.max_zoom_index = 10

-- Zoom level offsets for proper player centering
-- These offsets compensate for the player marker appearing off-center at higher zoom levels
-- Values correspond to navigation button clicks: East = +X, South = -Z
persistent_map.zoom_offsets = {
	[1] = {x = 0, z = 0},    -- 0.5x zoom - no offset needed
	[2] = {x = 0, z = 0},    -- 1.0x zoom - no offset needed
	[3] = {x = 0, z = 0},    -- 2.0x zoom - no offset needed
	[4] = {x = 1, z = -1},   -- 4.0x zoom - 1 east, 1 south (1 east = +1 x, 1 south = -1 z)
	[5] = {x = 2, z = -1},   -- 6.0x zoom - 1 east, 1 south
	[6] = {x = 2, z = -2},   -- 8.0x zoom - 2 east, 1 south (2 east = +2 x, 1 south = -1 z)
	[7] = {x = 2, z = -2},   -- 10.0x zoom - 2 east, 2 south (2 east = +2 x, 2 south = -2 z)
	[8] = {x = 2, z = -2},   -- 13.0x zoom - 2 east, 2 south
	[9] = {x = 2, z = -2},   -- 16.0x zoom - 2 east, 2 south
	[10] = {x = 2, z = -2},  -- 20.0x zoom - 2 east, 2 south
}

-- Zoom level X offsets for marker name text positioning
-- These offsets adjust marker name text X position at different zoom levels
-- Positive values move text right, negative values move text left
persistent_map.marker_name_zoom_x_offsets = {
	[1] = 0.0,    -- 0.5x zoom - no offset needed
	[2] = 0.0,    -- 1.0x zoom - no offset needed
	[3] = 0.0,    -- 2.0x zoom - no offset needed
	[4] = 5,    -- 4.0x zoom - slight right offset
	[5] = 10,    -- 6.0x zoom - moderate right offset
	[6] = 20,    -- 8.0x zoom - more right offset
	[7] = 25,    -- 10.0x zoom - significant right offset
	[8] = 30,    -- 13.0x zoom - large right offset
	[9] = 30,    -- 16.0x zoom - very large right offset
	[10] = 30,   -- 20.0x zoom - maximum right offset
}

-- World configuration for total tile calculation
persistent_map.world_size = 64000 -- blocks per world side (64000x64000 world)
persistent_map.total_world_tiles = math.floor(persistent_map.world_size / persistent_map.tile_size) ^ 2 -- Total tiles in world

-- Marker system configuration
persistent_map.marker_colors = {
	{name = "Red", color = "#FF0000"},
	{name = "Blue", color = "#0000FF"},
	{name = "Green", color = "#00FF00"},
	{name = "Yellow", color = "#FFFF00"},
	{name = "Purple", color = "#FF00FF"},
	{name = "Orange", color = "#FF8000"},
	{name = "White", color = "#FFFFFF"},
	{name = "Pink", color = "#FF80FF"}
}
persistent_map.marker_delete_distance = 25 -- blocks within which player can delete markers

local modname = minetest.get_current_modname()
local modpath = minetest.get_modpath(modname)
local S = minetest.get_translator(modname)

-- Storage
local storage = minetest.get_mod_storage()
local worldpath = minetest.get_worldpath()
local map_path = worldpath .. "/persistent_maps/"
minetest.mkdir(map_path)

-- Make storage accessible to other modules
persistent_map.storage = storage
persistent_map.map_path = map_path

-- Player data - isolated per player
local player_data = {}
local map_view_offset = {} -- Stores view offset for each player
local map_zoom_level = {} -- Stores zoom level index for each player
local nodecolors = dofile(modpath .. "/nodecolors.lua")

-- Cache for node colors to avoid repeated lookups
local node_color_cache = {}

-- Get color for a node with caching
local function get_node_color(nodename)
	local cached_color = node_color_cache[nodename]
	if cached_color then
		return cached_color
	end
	
	local color = nodecolors.get_color(nodename)
	node_color_cache[nodename] = color
	return color
end

-- Convert world position to tile coordinates with proper orientation
-- X increases East, Z increases North
local function pos_to_tile_coords(pos)
	return math.floor(pos.x / persistent_map.tile_size),
	       math.floor(pos.z / persistent_map.tile_size)
end

-- Convert tile coordinates to world position (center of tile)
local function tile_coords_to_pos(tile_x, tile_z)
	return vector.new(
		tile_x * persistent_map.tile_size + persistent_map.tile_size / 2,
		0,
		tile_z * persistent_map.tile_size + persistent_map.tile_size / 2
	)
end

-- Get tile ID from coordinates
local function get_tile_id(tile_x, tile_z)
	return string.format("tile_%d_%d", tile_x, tile_z)
end

-- Check if tile exists on disk
local function tile_exists(tile_id)
	local filename = map_path .. tile_id .. ".png"
	local file = io.open(filename, "r")
	if file then
		file:close()
		return true
	end
	return false
end

-- Marker system functions
local function load_player_markers(player_name)
	local data = storage:get_string("markers_" .. player_name)
	if data == "" then
		return {}
	end
	local markers = minetest.deserialize(data) or {}
	
	-- Migrate existing markers to include names if they don't have them
	for marker_id, marker in pairs(markers) do
		if not marker.name then
			marker.name = string.format("Marker %d,%d", math.floor(marker.x), math.floor(marker.z))
		end
	end
	
	return markers
end

local function save_player_markers(player_name, markers)
	storage:set_string("markers_" .. player_name, minetest.serialize(markers))
end

local function add_marker(player_name, pos, color_index, name)
	if not player_data[player_name] then
		return false
	end
	
	local markers = player_data[player_name].markers
	local marker_id = string.format("marker_%d_%d_%d", math.floor(pos.x), math.floor(pos.y), math.floor(pos.z))
	
	-- Check if marker already exists at this position
	if markers[marker_id] then
		return false, "Marker already exists at this position"
	end
	
	-- Add marker with name
	markers[marker_id] = {
		x = pos.x,
		y = pos.y,
		z = pos.z,
		color_index = color_index,
		name = name or "Unnamed",
		timestamp = os.time()
	}
	
	save_player_markers(player_name, markers)
	return true, "Marker '" .. (name or "Unnamed") .. "' placed"
end

local function remove_marker_near_player(player_name, player_pos)
	if not player_data[player_name] then
		return false, "Player data not found"
	end
	
	local markers = player_data[player_name].markers
	local removed_count = 0
	
	-- Find markers within delete distance
	for marker_id, marker in pairs(markers) do
		local marker_pos = vector.new(marker.x, marker.y, marker.z)
		local distance = vector.distance(player_pos, marker_pos)
		
		if distance <= persistent_map.marker_delete_distance then
			markers[marker_id] = nil
			removed_count = removed_count + 1
		end
	end
	
	if removed_count > 0 then
		save_player_markers(player_name, markers)
		return true, string.format("Removed %d marker(s)", removed_count)
	else
		return false, "No markers found nearby"
	end
end

-- Delete all markers for a player
local function delete_all_markers(player_name)
	if not player_data[player_name] then
		return false, "Player data not found"
	end
	
	-- Clear markers only
	player_data[player_name].markers = {}
	save_player_markers(player_name, {})
	
	return true, "All markers deleted"
end

-- Accessor functions for marker-gui.lua
function persistent_map.get_player_data(player_name)
	return player_data[player_name]
end

function persistent_map.get_map_view_offset(player_name)
	return map_view_offset[player_name]
end

function persistent_map.set_map_view_offset(player_name, offset)
	map_view_offset[player_name] = offset
end

function persistent_map.get_map_zoom_level(player_name)
	return map_zoom_level[player_name]
end

function persistent_map.set_map_zoom_level(player_name, zoom_level)
	map_zoom_level[player_name] = zoom_level
end

function persistent_map.delete_all_markers_for_player(player_name)
	return delete_all_markers(player_name)
end

function persistent_map.save_markers_for_player(player_name, markers)
	return save_player_markers(player_name, markers)
end

function persistent_map.add_marker_for_player(player_name, pos, name, color_index)
	return add_marker(player_name, pos, color_index or 1, name)
end

-- Generate a single map tile with proper coordinate mapping
local function generate_tile(tile_x, tile_z, callback)
	local tile_id = get_tile_id(tile_x, tile_z)
	
	-- Skip if already exists
	if tile_exists(tile_id) then
		if callback then callback(tile_id) end
		return tile_id
	end
	
	-- Calculate world bounds for this tile
	local minp = vector.new(
		tile_x * persistent_map.tile_size,
		persistent_map.min_height,
		tile_z * persistent_map.tile_size
	)
	local maxp = vector.new(
		(tile_x + 1) * persistent_map.tile_size - 1,
		persistent_map.max_height,
		(tile_z + 1) * persistent_map.tile_size - 1
	)
	
	-- Emerge area first
	minetest.emerge_area(minp, maxp, function(blockpos, action, calls_remaining)
		if calls_remaining > 0 then return end
		
		-- Generate pixel data with proper orientation
		-- Image coordinates: (0,0) at top-left, x increases right, y increases down
		-- World coordinates: x increases east, z increases north
		local pixels = {}
		local pixel_count = persistent_map.tile_size * persistent_map.tile_size
		
		-- Pre-allocate pixel array for better performance
		for i = 1, pixel_count do
			pixels[i] = ""
		end
		
		local tile_size = persistent_map.tile_size
		local min_x, max_z = minp.x, maxp.z
		local max_y, min_y = maxp.y, minp.y
		local default_color = string.char(persistent_map.generation.default_color_r, persistent_map.generation.default_color_g, persistent_map.generation.default_color_b, persistent_map.generation.alpha_value)
		
		-- Iterate through world coordinates properly
		-- For image: top-to-bottom corresponds to north-to-south (z decreasing)
		-- For image: left-to-right corresponds to west-to-east (x increasing)
		local pixel_index = 1
		for img_y = 0, tile_size - 1 do
			local world_z = max_z - img_y  -- Flip Z to match image orientation
			for img_x = 0, tile_size - 1 do
				local world_x = min_x + img_x
				
				local color = default_color
				
				-- Scan from top to bottom for surface node
				for y = max_y, min_y, -1 do
					local node = minetest.get_node({x=world_x, y=y, z=world_z})
					if node.name ~= "air" and node.name ~= "ignore" then
						local node_color = get_node_color(node.name)
						color = string.char(node_color[1], node_color[2], node_color[3], persistent_map.generation.alpha_value)
						break
					end
				end
				
				pixels[pixel_index] = color
				pixel_index = pixel_index + 1
			end
		end
		
		-- Save as PNG
		local filename = map_path .. tile_id .. ".png"
		local png_data = minetest.encode_png(
			persistent_map.tile_size,
			persistent_map.tile_size,
			table.concat(pixels)
		)
		minetest.safe_file_write(filename, png_data)
		
		-- Load texture dynamically
		minetest.dynamic_add_media(filename, function()
			if callback then callback(tile_id) end
		end)
	end)
	
	return tile_id
end

-- Load player discovered tiles
local function load_player_tiles(player_name)
	local data = storage:get_string("player_" .. player_name)
	if data == "" then
		return {}
	end
	return minetest.deserialize(data) or {}
end

-- Save player discovered tiles
local function save_player_tiles(player_name, tiles)
	storage:set_string("player_" .. player_name, minetest.serialize(tiles))
end

-- Check if a tile is discovered by a specific player
local function is_tile_discovered(player_name, tile_x, tile_z)
	if not player_data[player_name] then
		return false
	end
	
	local tiles = player_data[player_name].discovered_tiles
	local tile_id = get_tile_id(tile_x, tile_z)
	return tiles[tile_id] ~= nil
end

-- Add discovered tile for player
local function add_discovered_tile(player_name, tile_x, tile_z, callback)
	if not player_data[player_name] then
		return false
	end
	
	local tiles = player_data[player_name].discovered_tiles
	local tile_id = get_tile_id(tile_x, tile_z)
	
	-- Check if already discovered
	if tiles[tile_id] then
		return false
	end
	
	-- Add to player's discovered tiles
	tiles[tile_id] = {x = tile_x, z = tile_z}
	save_player_tiles(player_name, tiles)
	
	-- Generate tile if it doesn't exist (shared resource)
	generate_tile(tile_x, tile_z, function(id)
		minetest.log("action", "[persistent_map] Generated tile: " .. id .. " for " .. player_name)
		
		-- Send the tile to this specific player
		local filename = map_path .. tile_id .. ".png"
		minetest.dynamic_add_media({
			filepath = filename,
			to_player = player_name,
		}, function(pname)
			minetest.log("action", "[persistent_map] Sent tile " .. id .. " to " .. pname)
			-- Call the callback after tile generation and sending is complete
			if callback then
				callback()
			end
		end)
	end)
	
	return true
end

-- Get player's position within their current tile (0.0 to 1.0)
local function get_position_in_tile(pos)
	local tile_x, tile_z = pos_to_tile_coords(pos)
	local tile_world_x = tile_x * persistent_map.tile_size
	local tile_world_z = tile_z * persistent_map.tile_size
	
	local offset_x = (pos.x - tile_world_x) / persistent_map.tile_size
	local offset_z = (pos.z - tile_world_z) / persistent_map.tile_size
	
	return offset_x, offset_z
end

-- Show map GUI with player-centered view and proper orientation
function persistent_map.show_map(player_name)
	local player = minetest.get_player_by_name(player_name)
	if not player then return end
	
	-- Ensure player data exists
	if not player_data[player_name] then
		minetest.log("warning", "[persistent_map] Player data not found for " .. player_name)
		return
	end
	
	local tiles = player_data[player_name].discovered_tiles
	local markers = player_data[player_name].markers
	local pos = player:get_pos()
	local center_tile_x, center_tile_z = pos_to_tile_coords(pos)
	
	-- Get current view offset (defaults to 0, 0 - centered on player)
	local view_offset = map_view_offset[player_name] or {x = 0, z = 0}
	
	-- Get current zoom level (defaults to default zoom)
	local zoom_index = map_zoom_level[player_name] or persistent_map.default_zoom_index
	local zoom_factor = persistent_map.zoom_levels[zoom_index]
	
	-- Get zoom-specific offset for proper player centering
	local zoom_offset = persistent_map.zoom_offsets[zoom_index] or {x = 0, z = 0}
	
	-- Apply both manual view offset and zoom-specific centering offset
	local view_center_x = center_tile_x + view_offset.x + zoom_offset.x
	local view_center_z = center_tile_z + view_offset.z + zoom_offset.z
	
	-- Calculate player's precise position within the tile (for marker placement)
	local offset_x, offset_z = get_position_in_tile(pos)
	
	-- View configuration - adjust radius based on zoom to keep reasonable map size
	-- At higher zoom levels, show fewer tiles but make them much larger
	local base_radius = persistent_map.view_radius
	local radius = math.max(persistent_map.generation.min_radius, math.floor(base_radius / zoom_factor))
	local view_size = radius * 2 + 1
	local tile_display_size = persistent_map.gui_tile_size * zoom_factor
	
	-- FIXED LAYOUT: Static formspec size regardless of zoom
	-- Use base tile size for layout calculations to keep formspec static
	local base_tile_size = persistent_map.gui_tile_size
	local base_view_size = persistent_map.view_radius * 2 + 1
	local map_width = base_view_size * base_tile_size
	local map_height = base_view_size * base_tile_size
	
	-- Side panel dimensions
	local side_panel_width = persistent_map.ui.side_panel_width
	local padding = persistent_map.ui.padding
	local header_height = persistent_map.ui.header_height
	local button_size = persistent_map.ui.button_size
	local left_panel_x = padding
	local map_x = left_panel_x + side_panel_width + padding
	local right_panel_x = map_x + map_width + padding
	
	-- Total formspec dimensions
	local total_width = padding + side_panel_width + padding + map_width + padding + side_panel_width + padding
	local total_height = math.max(map_height + header_height + padding * 2, persistent_map.ui.min_formspec_height) -- Ensure minimum height for controls
	
	-- Pre-calculate commonly used values
	local half_total_width = total_width * persistent_map.ui.total_width_factor
	local header_y = padding
	local map_start_x = map_x
	local map_start_y = padding + header_height
	
	-- Use table for better performance than repeated string.format calls
	local formspec = {}
	formspec[1] = string.format("formspec_version[%d]", persistent_map.ui.formspec_version)
	formspec[2] = string.format("size[%.2f,%.2f]", total_width, total_height)
	formspec[3] = "bgcolor[#000000FF;true]"
	formspec[4] = string.format("label[%.2f,%.2f;Discovered Map - Close with ESC]", half_total_width - persistent_map.ui.header_title_offset, padding)
	local formspec_index = 5
	
	-- Add scrollable container for the map area
	local scroll_width = map_width
	local scroll_height = map_height
	local actual_content_width = view_size * tile_display_size
	local actual_content_height = view_size * tile_display_size
	
	formspec[formspec_index] = string.format(
		"scroll_container[%.2f,%.2f;%.2f,%.2f;map_scroll;horizontal;%.2f]",
		map_x, map_start_y, scroll_width, scroll_height,
		math.max(0, (actual_content_width - scroll_width) * persistent_map.ui.scroll_content_center)
	)
	formspec_index = formspec_index + 1
	
	-- Count discovered tiles in view
	local discovered_count = 0
	local total_tiles = view_size * view_size
	
	-- Pre-calculate values for the tile loop
	local player_tiles = player_data[player_name].discovered_tiles
	
	-- Draw tiles in a grid with proper orientation
	-- GUI grid: (0,0) at top-left, x increases right, y increases down
	-- World grid: x increases east, z increases north
	-- Map display: North at top, East at right
	for gui_row = 0, view_size - 1 do
		local tile_z_base = view_center_z + radius - gui_row  -- Pre-calculate Z base
		local gui_y = gui_row * tile_display_size
		
		for gui_col = 0, view_size - 1 do
			-- Convert GUI grid position to world tile coordinates
			local tile_x = view_center_x + gui_col - radius
			local tile_z = tile_z_base
			
			-- Calculate GUI position within scroll container (relative to container origin)
			local gui_x = gui_col * tile_display_size
			
			-- Check if THIS PLAYER has discovered this tile (optimized lookup)
			local tile_id = string.format("tile_%d_%d", tile_x, tile_z)
			if player_tiles[tile_id] then
				discovered_count = discovered_count + 1
				
				-- Verify tile exists before trying to display
				if tile_exists(tile_id) then
					formspec[formspec_index] = string.format(
						"image[%.2f,%.2f;%.2f,%.2f;%s.png]",
						gui_x, gui_y, tile_display_size, tile_display_size, tile_id
					)
				else
					-- Tile should exist but doesn't - draw as fog
					formspec[formspec_index] = string.format(
						"box[%.2f,%.2f;%.2f,%.2f;#404040]",
						gui_x, gui_y, tile_display_size, tile_display_size
					)
				end
			else
				-- Draw undiscovered tile (fog of war)
				formspec[formspec_index] = string.format(
					"box[%.2f,%.2f;%.2f,%.2f;#000000]",
					gui_x, gui_y, tile_display_size, tile_display_size
				)
			end
			formspec_index = formspec_index + 1
		end
	end
	
	-- Draw markers on the map (optimized)
	local marker_scale_factor = persistent_map.map_marker.scale_factor * persistent_map.marker_scale * zoom_factor
	local half_marker_size = marker_scale_factor * persistent_map.map_marker.half_size_factor
	local tile_size_f = persistent_map.tile_size
	
	for marker_id, marker in pairs(markers) do
		-- Inline tile coordinate calculation for better performance
		local marker_tile_x = math.floor(marker.x / tile_size_f)
		local marker_tile_z = math.floor(marker.z / tile_size_f)
		
		-- Check if marker is within visible area
		local marker_offset_x = marker_tile_x - view_center_x
		local marker_offset_z = marker_tile_z - view_center_z
		
		if math.abs(marker_offset_x) <= radius and math.abs(marker_offset_z) <= radius then
			-- Convert world tile offset to GUI position
			local gui_col = marker_offset_x + radius
			local gui_row = radius - marker_offset_z  -- Flip Z for display
			
			local marker_tile_gui_x = gui_col * tile_display_size
			local marker_tile_gui_y = gui_row * tile_display_size
			
			-- Calculate precise position within tile (inlined for performance)
			local tile_world_x = marker_tile_x * tile_size_f
			local tile_world_z = marker_tile_z * tile_size_f
			local marker_offset_in_tile_x = (marker.x - tile_world_x) / tile_size_f
			local marker_offset_in_tile_z = (marker.z - tile_world_z) / tile_size_f
			
			local marker_gui_x = marker_tile_gui_x + (marker_offset_in_tile_x * tile_display_size)
			local marker_gui_y = marker_tile_gui_y + ((1.0 - marker_offset_in_tile_z) * tile_display_size)  -- Flip Z offset
			
			-- Draw marker dot (scaled with zoom) - stack 5 transparent squares for solid appearance
			local color = persistent_map.marker_colors[marker.color_index] or persistent_map.marker_colors[1]
			for stack_layer = 1, 5 do
				formspec[formspec_index] = string.format(
					"box[%.2f,%.2f;%.2f,%.2f;%s]",
					marker_gui_x - half_marker_size, marker_gui_y - half_marker_size,
					marker_scale_factor, marker_scale_factor, color.color
				)
				formspec_index = formspec_index + 1
			end
			
			-- Draw marker name above the marker (if it has a name and zoom is sufficient)
			local marker_name = marker.name or "Unnamed"
			if marker_name ~= "" and zoom_factor >= persistent_map.marker_name_min_zoom then  -- Show names at lower zoom levels too
				-- Scale text size with zoom factor using configurable values
				local text_scale = math.max(persistent_map.marker_name_scale, zoom_factor * persistent_map.marker_name_zoom_multiplier)
				local char_width = persistent_map.marker_name_char_width * text_scale  -- Character width estimation scaled
				local text_height = persistent_map.marker_name_text_height * text_scale  -- Text height scaled
				
				local name_y = marker_gui_y - half_marker_size - (text_height + persistent_map.marker_name_padding)  -- Position above marker with configurable padding
				local text_width = string.len(marker_name) * char_width
				local name_x = marker_gui_x - (text_width * persistent_map.map_marker.text_center_factor)  -- Center text properly
				
				-- Apply zoom-specific X offset for marker name positioning
				local marker_name_x_offset = persistent_map.marker_name_zoom_x_offsets[zoom_index] or 0.0
				name_x = name_x + marker_name_x_offset
				
				-- Add the marker name label with configurable font size
				formspec[formspec_index] = string.format(
					"style[marker_name_%s;font_size=+%d]",
					marker_id, math.floor(text_scale * persistent_map.marker_name_font_multiplier)
				)
				formspec_index = formspec_index + 1
				
				formspec[formspec_index] = string.format(
					"label[%.2f,%.2f;%s]",
					name_x, name_y, minetest.formspec_escape(marker_name)
				)
				formspec_index = formspec_index + 1
			end
		end
	end
	
	-- Draw player position marker with proper orientation
	local player_tile_offset_x = center_tile_x - view_center_x
	local player_tile_offset_z = center_tile_z - view_center_z
	
	-- Check if player is within visible grid
	if math.abs(player_tile_offset_x) <= radius and math.abs(player_tile_offset_z) <= radius then
		-- Convert world tile offset to GUI position
		local gui_col = player_tile_offset_x + radius
		local gui_row = radius - player_tile_offset_z  -- Flip Z for display
		
		local marker_tile_x = gui_col * tile_display_size
		local marker_tile_y = gui_row * tile_display_size
		
		-- Add the offset within the current tile
		-- Note: offset_z needs to be flipped for display (north = negative GUI y)
		local marker_x = marker_tile_x + (offset_x * tile_display_size)
		local marker_y = marker_tile_y + ((persistent_map.map_marker.position_flip_factor - offset_z) * tile_display_size)  -- Flip Z offset
		
		-- Get player's look direction (yaw is in radians)
		local yaw = player:get_look_horizontal()
		-- Convert yaw to compass direction (0 = north, π/2 = east, π = south, 3π/2 = west)
		-- In Luanti: yaw 0 = north, π/2 = west, π = south, 3π/2 = east
		-- We need to convert this to degrees and adjust for our triangle (which points north at 0°)
		local angle = math.deg(-yaw)  -- Convert to degrees, no additional offset needed
		
		-- Create a triangular arrow pointing in the direction the player is facing (scaled with zoom)
		local scale = persistent_map.player_marker_scale * zoom_factor
		local size = persistent_map.player_marker.arrow_size * scale
		local arrow_height = persistent_map.player_marker.arrow_height * scale
		
		-- Calculate rotation matrix for the triangle
		local rad = math.rad(angle)
		local cos_a = math.cos(rad)
		local sin_a = math.sin(rad)
		
		-- Define triangle points (pointing up initially = north)
		local x1, y1 = 0, -arrow_height
		local x2, y2 = -size, arrow_height * persistent_map.ui.arrow_height_factor
		local x3, y3 = size, arrow_height * persistent_map.ui.arrow_height_factor
		
		-- Rotate points
		local rx1 = x1 * cos_a - y1 * sin_a
		local ry1 = x1 * sin_a + y1 * cos_a
		local rx2 = x2 * cos_a - y2 * sin_a
		local ry2 = x2 * sin_a + y2 * cos_a
		local rx3 = x3 * cos_a - y3 * sin_a
		local ry3 = x3 * sin_a + y3 * cos_a
		
		-- Translate to marker position
		local p1x, p1y = marker_x + rx1, marker_y + ry1
		
		-- Draw filled triangle using boxes to approximate it (all scaled) - stack 5 for solid appearance
		-- Main body (red square at center)
		local main_size = persistent_map.player_marker.main_body_size * scale
		local half_main = main_size * persistent_map.map_marker.half_size_factor
		for stack_layer = 1, 5 do
			formspec[formspec_index] = string.format(
				"box[%.2f,%.2f;%.2f,%.2f;#FF0000]",
				marker_x - half_main, marker_y - half_main, main_size, main_size
			)
			formspec_index = formspec_index + 1
		end
		
		-- Draw directional indicator line (pointing direction - the tip)
		local tip_size = persistent_map.player_marker.tip_size * scale
		local half_tip = tip_size * persistent_map.map_marker.half_size_factor
		for stack_layer = 1, 5 do
			formspec[formspec_index] = string.format(
				"box[%.2f,%.2f;%.2f,%.2f;#FF0000]",
				p1x - half_tip, p1y - half_tip, tip_size, tip_size
			)
			formspec_index = formspec_index + 1
		end
		
		-- Additional points to make the arrow more visible (mid-section)
		local mid1x = marker_x + (rx1 * persistent_map.player_marker.mid_section_factor)
		local mid1y = marker_y + (ry1 * persistent_map.player_marker.mid_section_factor)
		local mid_size = persistent_map.player_marker.mid_section_size * scale
		local half_mid = mid_size * persistent_map.map_marker.half_size_factor
		for stack_layer = 1, 5 do
			formspec[formspec_index] = string.format(
				"box[%.2f,%.2f;%.2f,%.2f;#FF0000]",
				mid1x - half_mid, mid1y - half_mid, mid_size, mid_size
			)
			formspec_index = formspec_index + 1
		end
		
		-- Wing indicators (base of arrow)
		local wing1x = marker_x + (rx2 * persistent_map.player_marker.wing_factor)
		local wing1y = marker_y + (ry2 * persistent_map.player_marker.wing_factor)
		local wing2x = marker_x + (rx3 * persistent_map.player_marker.wing_factor)
		local wing2y = marker_y + (ry3 * persistent_map.player_marker.wing_factor)
		local wing_size = persistent_map.player_marker.wing_size * scale
		local half_wing = wing_size * persistent_map.map_marker.half_size_factor
		
		for stack_layer = 1, 5 do
			formspec[formspec_index] = string.format(
				"box[%.2f,%.2f;%.2f,%.2f;#FF0000]",
				wing1x - half_wing, wing1y - half_wing, wing_size, wing_size
			)
			formspec_index = formspec_index + 1
		end
		
		for stack_layer = 1, 5 do
			formspec[formspec_index] = string.format(
				"box[%.2f,%.2f;%.2f,%.2f;#FF0000]",
				wing2x - half_wing, wing2y - half_wing, wing_size, wing_size
			)
			formspec_index = formspec_index + 1
		end
	end
	
	-- Draw party member markers (if player is in a party)
	if persistent_map.get_party_member_positions then
		local party_members = persistent_map.get_party_member_positions(player_name)
		local party_marker_scale = persistent_map.player_marker_scale * zoom_factor * 0.8 -- Slightly smaller than main player
		
		for member_name, member_data in pairs(party_members) do
			local member_pos = member_data.pos
			local member_yaw = member_data.yaw
			
			-- Convert member position to tile coordinates
			local member_tile_x, member_tile_z = pos_to_tile_coords(member_pos)
			local member_tile_offset_x = member_tile_x - view_center_x
			local member_tile_offset_z = member_tile_z - view_center_z
			
			-- Check if member is within visible grid
			if math.abs(member_tile_offset_x) <= radius and math.abs(member_tile_offset_z) <= radius then
				-- Convert world tile offset to GUI position
				local gui_col = member_tile_offset_x + radius
				local gui_row = radius - member_tile_offset_z  -- Flip Z for display
				
				local member_tile_x_gui = gui_col * tile_display_size
				local member_tile_y_gui = gui_row * tile_display_size
				
				-- Calculate precise position within tile
				local member_offset_x, member_offset_z = get_position_in_tile(member_pos)
				local member_marker_x = member_tile_x_gui + (member_offset_x * tile_display_size)
				local member_marker_y = member_tile_y_gui + ((persistent_map.map_marker.position_flip_factor - member_offset_z) * tile_display_size)
				
				-- Draw party member marker (blue triangle)
				local member_angle = math.deg(-member_yaw)
				local member_scale = party_marker_scale
				local member_size = persistent_map.player_marker.arrow_size * member_scale
				local member_arrow_height = persistent_map.player_marker.arrow_height * member_scale
				
				-- Calculate rotation matrix for the triangle
				local member_rad = math.rad(member_angle)
				local member_cos_a = math.cos(member_rad)
				local member_sin_a = math.sin(member_rad)
				
				-- Define triangle points (pointing up initially = north)
				local mx1, my1 = 0, -member_arrow_height
				local mx2, my2 = -member_size, member_arrow_height * persistent_map.ui.arrow_height_factor
				local mx3, my3 = member_size, member_arrow_height * persistent_map.ui.arrow_height_factor
				
				-- Rotate points
				local mrx1 = mx1 * member_cos_a - my1 * member_sin_a
				local mry1 = mx1 * member_sin_a + my1 * member_cos_a
				local mrx2 = mx2 * member_cos_a - my2 * member_sin_a
				local mry2 = mx2 * member_sin_a + my2 * member_cos_a
				local mrx3 = mx3 * member_cos_a - my3 * member_sin_a
				local mry3 = mx3 * member_sin_a + my3 * member_cos_a
				
				-- Main body (blue square at center) - stack 5 for solid appearance
				local member_main_size = persistent_map.player_marker.main_body_size * member_scale
				local member_half_main = member_main_size * persistent_map.map_marker.half_size_factor
				for stack_layer = 1, 5 do
					formspec[formspec_index] = string.format(
						"box[%.2f,%.2f;%.2f,%.2f;#0080FF]",
						member_marker_x - member_half_main, member_marker_y - member_half_main, member_main_size, member_main_size
					)
					formspec_index = formspec_index + 1
				end
				
				-- Draw directional indicator (the tip)
				local member_p1x, member_p1y = member_marker_x + mrx1, member_marker_y + mry1
				local member_tip_size = persistent_map.player_marker.tip_size * member_scale
				local member_half_tip = member_tip_size * persistent_map.map_marker.half_size_factor
				for stack_layer = 1, 5 do
					formspec[formspec_index] = string.format(
						"box[%.2f,%.2f;%.2f,%.2f;#0080FF]",
						member_p1x - member_half_tip, member_p1y - member_half_tip, member_tip_size, member_tip_size
					)
					formspec_index = formspec_index + 1
				end
				
				-- Mid-section indicator
				local member_mid1x = member_marker_x + (mrx1 * persistent_map.player_marker.mid_section_factor)
				local member_mid1y = member_marker_y + (mry1 * persistent_map.player_marker.mid_section_factor)
				local member_mid_size = persistent_map.player_marker.mid_section_size * member_scale
				local member_half_mid = member_mid_size * persistent_map.map_marker.half_size_factor
				for stack_layer = 1, 5 do
					formspec[formspec_index] = string.format(
						"box[%.2f,%.2f;%.2f,%.2f;#0080FF]",
						member_mid1x - member_half_mid, member_mid1y - member_half_mid, member_mid_size, member_mid_size
					)
					formspec_index = formspec_index + 1
				end
				
				-- Wing indicators (base of arrow)
				local member_wing1x = member_marker_x + (mrx2 * persistent_map.player_marker.wing_factor)
				local member_wing1y = member_marker_y + (mry2 * persistent_map.player_marker.wing_factor)
				local member_wing2x = member_marker_x + (mrx3 * persistent_map.player_marker.wing_factor)
				local member_wing2y = member_marker_y + (mry3 * persistent_map.player_marker.wing_factor)
				local member_wing_size = persistent_map.player_marker.wing_size * member_scale
				local member_half_wing = member_wing_size * persistent_map.map_marker.half_size_factor
				
				for stack_layer = 1, 5 do
					formspec[formspec_index] = string.format(
						"box[%.2f,%.2f;%.2f,%.2f;#0080FF]",
						member_wing1x - member_half_wing, member_wing1y - member_half_wing, member_wing_size, member_wing_size
					)
					formspec_index = formspec_index + 1
				end
				
				for stack_layer = 1, 5 do
					formspec[formspec_index] = string.format(
						"box[%.2f,%.2f;%.2f,%.2f;#0080FF]",
						member_wing2x - member_half_wing, member_wing2y - member_half_wing, member_wing_size, member_wing_size
					)
					formspec_index = formspec_index + 1
				end
				
				-- Draw player name above the marker (if zoom is sufficient)
				if zoom_factor >= persistent_map.marker_name_min_zoom then
					local name_scale = math.max(persistent_map.marker_name_scale, zoom_factor * persistent_map.marker_name_zoom_multiplier)
					local name_char_width = persistent_map.marker_name_char_width * name_scale
					local name_text_height = persistent_map.marker_name_text_height * name_scale
					
					local name_y = member_marker_y - member_half_main - (name_text_height + persistent_map.marker_name_padding)
					local name_text_width = string.len(member_name) * name_char_width
					local name_x = member_marker_x - (name_text_width * persistent_map.map_marker.text_center_factor)
					
					-- Apply zoom-specific X offset for party member name positioning
					local member_name_x_offset = persistent_map.marker_name_zoom_x_offsets[zoom_index] or 0.0
					name_x = name_x + member_name_x_offset
					
					-- Add the member name label
					formspec[formspec_index] = string.format(
						"style[party_member_%s;font_size=+%d]",
						member_name, math.floor(name_scale * persistent_map.marker_name_font_multiplier)
					)
					formspec_index = formspec_index + 1
					
					formspec[formspec_index] = string.format(
						"label[%.2f,%.2f;%s]",
						name_x, name_y, minetest.formspec_escape(member_name)
					)
					formspec_index = formspec_index + 1
				end
			end
		end
	end
	
	-- Close scroll container
	formspec[formspec_index] = "scroll_container_end[]"
	formspec_index = formspec_index + 1
	
	-- LEFT PANEL: Navigation Controls
	local left_panel_y = padding + header_height + persistent_map.ui.nav_panel_offset
	
	-- Navigation section header
	formspec[formspec_index] = string.format(
		"label[%.2f,%.2f;Navigation:]",
		left_panel_x, left_panel_y
	)
	formspec_index = formspec_index + 1
	
	local nav_start_y = left_panel_y + persistent_map.ui.nav_section_spacing
	local nav_center_x = left_panel_x + side_panel_width / 2
	
	-- North button (up on map = north = +Z)
	table.insert(formspec, string.format(
		"button[%f,%f;%f,%f;nav_north;N ↑]",
		nav_center_x - button_size / persistent_map.ui.half_divisor, nav_start_y, button_size, button_size
	))
	
	-- West and East buttons
	table.insert(formspec, string.format(
		"button[%f,%f;%f,%f;nav_west;W ←]",
		nav_center_x - button_size * persistent_map.ui.nav_button_multiplier, nav_start_y + button_size + persistent_map.ui.nav_button_spacing, button_size, button_size
	))
	
	table.insert(formspec, string.format(
		"button[%f,%f;%f,%f;nav_east;E →]",
		nav_center_x + persistent_map.ui.nav_button_spacing, nav_start_y + button_size + persistent_map.ui.nav_button_spacing, button_size, button_size
	))
	
	-- South button
	table.insert(formspec, string.format(
		"button[%f,%f;%f,%f;nav_south;S ↓]",
		nav_center_x - button_size / persistent_map.ui.half_divisor, nav_start_y + button_size * persistent_map.ui.padding_multiplier + persistent_map.ui.nav_button_spacing * persistent_map.ui.padding_multiplier, button_size, button_size
	))
	
	-- Center on player button
	table.insert(formspec, string.format(
		"button[%f,%f;%f,%f;nav_center;Center on Player]",
		left_panel_x, nav_start_y + button_size * 3 + persistent_map.ui.nav_center_button_offset, side_panel_width - persistent_map.ui.button_width_offset, button_size
	))
	
	-- Zoom controls
	local zoom_y = nav_start_y + button_size * 4 + persistent_map.ui.zoom_section_offset
	table.insert(formspec, string.format(
		"label[%f,%f;Zoom: %.1fx]",
		left_panel_x, zoom_y, zoom_factor
	))
	
	-- Zoom buttons side by side
	local zoom_button_width = (side_panel_width - persistent_map.ui.nav_center_button_offset) / persistent_map.ui.half_divisor
	table.insert(formspec, string.format(
		"button[%f,%f;%f,%f;zoom_out;Zoom Out (-)]",
		left_panel_x, zoom_y + persistent_map.ui.zoom_button_spacing, zoom_button_width, button_size
	))
	
	table.insert(formspec, string.format(
		"button[%f,%f;%f,%f;zoom_in;Zoom In (+)]",
		left_panel_x + zoom_button_width + persistent_map.ui.zoom_button_spacing, zoom_y + persistent_map.ui.zoom_button_spacing, zoom_button_width, button_size
	))
	
	-- RIGHT PANEL: Marker Controls
	local right_panel_y = padding + header_height + persistent_map.ui.nav_panel_offset
	
	-- Marker section header
	table.insert(formspec, string.format(
		"label[%f,%f;Place Marker:]",
		right_panel_x, right_panel_y
	))
	
	-- Marker name input field
	local name_input_y = right_panel_y + persistent_map.ui.nav_section_spacing
	table.insert(formspec, string.format(
		"label[%f,%f;Marker Name:]",
		right_panel_x, name_input_y
	))
	
	table.insert(formspec, string.format(
		"field[%f,%f;%f,%f;marker_name;;]",
		right_panel_x, name_input_y + persistent_map.ui.marker_name_label_spacing, side_panel_width - persistent_map.ui.field_width_offset, persistent_map.ui.marker_name_field_height
	))
	
	-- Marker color buttons (2 columns in right panel)
	local marker_start_y = name_input_y + persistent_map.ui.marker_colors_offset
	local colors_per_row = persistent_map.ui.colors_per_row
	local color_button_size = persistent_map.ui.color_button_size
	
	for i, color_info in ipairs(persistent_map.marker_colors) do
		local row = math.floor((i - 1) / colors_per_row)
		local col = (i - 1) % colors_per_row
		local btn_x = right_panel_x + col * (color_button_size + persistent_map.ui.color_button_spacing)
		local btn_y = marker_start_y + row * (color_button_size + persistent_map.ui.color_button_row_spacing)
		
		-- Add colored background first
		table.insert(formspec, string.format(
			"box[%f,%f;%f,%f;%s]",
			btn_x, btn_y, color_button_size, color_button_size, color_info.color
		))
		
		table.insert(formspec, string.format(
			"button[%f,%f;%f,%f;add_marker_%d;%s]",
			btn_x, btn_y, color_button_size, color_button_size, i, color_info.name
		))
	end
	
	-- Manage Markers button (moved up since delete buttons are removed)
	local manage_y = marker_start_y + math.ceil(#persistent_map.marker_colors / colors_per_row) * (color_button_size + persistent_map.ui.color_button_row_spacing) + persistent_map.ui.delete_buttons_offset
	
	table.insert(formspec, string.format(
		"button[%f,%f;%f,%f;manage_markers;Manage Markers]",
		right_panel_x, manage_y, side_panel_width - persistent_map.ui.button_width_offset, button_size
	))
	
	-- Add coordinate display with discovery stats - positioned on the right side
	local offset_text = ""
	if view_offset.x ~= 0 or view_offset.z ~= 0 then
		offset_text = string.format(" (View offset: X%+d, Z%+d)", view_offset.x, view_offset.z)
	end
	
	-- Count total discovered tiles for this player
	local total_discovered = 0
	for _ in pairs(tiles) do
		total_discovered = total_discovered + 1
	end
	
	-- Count markers
	local marker_count = 0
	for _ in pairs(markers) do
		marker_count = marker_count + 1
	end
	
	-- BOTTOM INFO SECTION: Spread out information on both sides of the map
	local info_y = total_height - persistent_map.ui.info_bottom_margin
	local left_info_x = persistent_map.ui.info_left_align_with_panel and left_panel_x or padding
	local right_info_x = right_panel_x
	
	-- Left side information (2 pieces)
	table.insert(formspec, string.format(
		"label[%f,%f;Player Coords: X=%d Z=%d]",
		left_info_x, info_y,
		math.floor(pos.x), math.floor(pos.z)
	))
	
	table.insert(formspec, string.format(
		"label[%f,%f;Tile Coords: %d, %d]%s",
		left_info_x, info_y + persistent_map.ui.info_line_spacing,
		center_tile_x, center_tile_z, offset_text
	))
	
	-- Right side information (2 pieces)
	table.insert(formspec, string.format(
		"label[%f,%f;Discovered: %d/%d total tiles]",
		right_info_x, info_y,
		total_discovered, persistent_map.total_world_tiles
	))
	
	table.insert(formspec, string.format(
		"label[%f,%f;View: %d/%d tiles (%d markers)]",
		right_info_x, info_y + persistent_map.ui.info_line_spacing,
		discovered_count, total_tiles, marker_count
	))
	
	minetest.show_formspec(player_name, "persistent_map:map", table.concat(formspec))
end

-- Register chatcommand to open map
minetest.register_chatcommand("map", {
	description = S("Open the persistent map"),
	func = function(name)
		-- Reset view offset when opening map
		map_view_offset[name] = {x = 0, z = 0}
		-- Initialize zoom level if not set
		if not map_zoom_level[name] then
			map_zoom_level[name] = persistent_map.default_zoom_index
		end
		persistent_map.show_map(name)
		return true, S("Map opened")
	end,
})

-- Handle formspec input for navigation with proper orientation and marker system
minetest.register_on_player_receive_fields(function(player, formname, fields)
	if formname ~= "persistent_map:map" then return end
	
	local name = player:get_player_name()
	local offset = map_view_offset[name] or {x = 0, z = 0}
	local updated = false
	
	-- Navigation with proper orientation:
	-- North = +Z, South = -Z, East = +X, West = -X
	if fields.nav_north then
		offset.z = offset.z + 1  -- Move view north (+Z)
		updated = true
	elseif fields.nav_south then
		offset.z = offset.z - 1  -- Move view south (-Z)
		updated = true
	elseif fields.nav_west then
		offset.x = offset.x - 1  -- Move view west (-X)
		updated = true
	elseif fields.nav_east then
		offset.x = offset.x + 1  -- Move view east (+X)
		updated = true
	elseif fields.nav_center then
		offset.x = 0
		offset.z = 0
		updated = true
	elseif fields.zoom_in then
		-- Zoom in (increase zoom level)
		local current_zoom = map_zoom_level[name] or persistent_map.default_zoom_index
		if current_zoom < persistent_map.max_zoom_index then
			map_zoom_level[name] = current_zoom + 1
			updated = true
		end
	elseif fields.zoom_out then
		-- Zoom out (decrease zoom level)
		local current_zoom = map_zoom_level[name] or persistent_map.default_zoom_index
		if current_zoom > persistent_map.min_zoom_index then
			map_zoom_level[name] = current_zoom - 1
			updated = true
		end
	elseif fields.manage_markers then
		-- Open marker management GUI
		persistent_map.show_marker_gui(name)
		return -- Don't update the map, just switch to marker GUI
	else
		-- Check for marker placement buttons
		for i = 1, #persistent_map.marker_colors do
			if fields["add_marker_" .. i] then
				local pos = player:get_pos()
				local marker_name = fields.marker_name or ""
				-- If no name provided, use a default based on coordinates
				if marker_name == "" then
					marker_name = string.format("Marker %d,%d", math.floor(pos.x), math.floor(pos.z))
				end
				local success, message = add_marker(name, pos, i, marker_name)
				minetest.chat_send_player(name, message)
				updated = true
				break
			end
		end
	end
	
	if updated then
		map_view_offset[name] = offset
		persistent_map.show_map(name)
	end
end)

-- Player join - Initialize player data
minetest.register_on_joinplayer(function(player)
	local name = player:get_player_name()
	
	-- Load this player's discovered tiles
	local tiles = load_player_tiles(name)
	
	-- Load this player's markers
	local markers = load_player_markers(name)
	
	-- Initialize player data structure
	player_data[name] = {
		discovered_tiles = tiles,
		markers = markers,
		last_tile_x = nil,
		last_tile_z = nil,
	}
	
	-- Count tiles and markers efficiently
	local tile_count = 0
	for _ in pairs(tiles) do tile_count = tile_count + 1 end
	local marker_count = 0
	for _ in pairs(markers) do marker_count = marker_count + 1 end
	
	minetest.log("action", "[persistent_map] Player " .. name .. " joined with " .. tile_count .. " discovered tiles and " .. marker_count .. " markers")
	
	-- Re-send all of THIS PLAYER's discovered tile textures to the client
	for tile_id, _ in pairs(tiles) do
		local filename = map_path .. tile_id .. ".png"
		local file = io.open(filename, "r")
		if file then
			file:close()
			-- Dynamically add the media for this specific player
			minetest.dynamic_add_media({
				filepath = filename,
				to_player = name,
			}, function(player_name)
				minetest.log("action", "[persistent_map] Reloaded tile " .. tile_id .. " for " .. player_name)
			end)
		else
			minetest.log("warning", "[persistent_map] Tile file not found: " .. tile_id .. " for player " .. name)
		end
	end
	
	-- Trigger initial tile discovery for player's current position
	minetest.after(persistent_map.generation.initial_discovery_delay, function()
		local pos = player:get_pos()
		if pos then
			local tile_x, tile_z = pos_to_tile_coords(pos)
			local data = player_data[name]
			if data then
				local discovered = add_discovered_tile(name, tile_x, tile_z, function()
					-- Send discovery message only after tile generation is complete
					minetest.chat_send_player(name, S("New area discovered!"))
				end)
				if discovered then
					data.last_tile_x = tile_x
					data.last_tile_z = tile_z
				end
			end
		end
	end)
end)

-- Player leave - Clean up player data
minetest.register_on_leaveplayer(function(player)
	local name = player:get_player_name()
	player_data[name] = nil
	map_view_offset[name] = nil
	map_zoom_level[name] = nil
	minetest.log("action", "[persistent_map] Player " .. name .. " left, data cleaned up")
end)

-- Discovery scanner
local scan_timer = 0
minetest.register_globalstep(function(dtime)
	scan_timer = scan_timer + dtime
	if scan_timer < persistent_map.scan_interval then return end
	scan_timer = 0
	
	for _, player in ipairs(minetest.get_connected_players()) do
		local name = player:get_player_name()
		
		-- Ensure player data exists
		if not player_data[name] then
			minetest.log("warning", "[persistent_map] Player data missing for " .. name .. " in globalstep")
			goto continue
		end
		
		local pos = player:get_pos()
		local tile_x, tile_z = pos_to_tile_coords(pos)
		
		local data = player_data[name]
		
		-- Check if player moved to new tile
		if data.last_tile_x ~= tile_x or data.last_tile_z ~= tile_z then
			local discovered = add_discovered_tile(name, tile_x, tile_z, function()
				-- Send discovery message only after tile generation is complete
				minetest.chat_send_player(name, S("New area discovered!"))
			end)
			if discovered then
				data.last_tile_x = tile_x
				data.last_tile_z = tile_z
			end
		end
		
		::continue::
	end
end)

-- Register map book item
minetest.register_craftitem("discovery_maps:map_book", {
	description = S("Map Book") .. "\n" .. S("Right-click to open the persistent map"),
	inventory_image = "default_book.png^[colorize:#8B4513:120",
	stack_max = 1,
	on_use = function(itemstack, user, pointed_thing)
		if user and user:is_player() then
			local player_name = user:get_player_name()
			-- Reset view offset when opening map
			map_view_offset[player_name] = {x = 0, z = 0}
			-- Initialize zoom level if not set
			if not map_zoom_level[player_name] then
				map_zoom_level[player_name] = persistent_map.default_zoom_index
			end
			persistent_map.show_map(player_name)
		end
		return itemstack
	end,
})

-- Register craft recipe for map book
minetest.register_craft({
	output = "discovery_maps:map_book",
	recipe = {
		{"default:paper", "default:paper", "default:paper"},
		{"default:paper", "default:book", "default:paper"},
		{"default:paper", "default:paper", "default:paper"}
	}
})


minetest.register_craft({
	output = "discovery_maps:map_book",
	recipe = {
		{"mcl_core:paper", "mcl_core:paper", "mcl_core:paper"},
		{"mcl_core:paper", "mcl_books:writable_book", "mcl_core:paper"},
		{"mcl_core:paper", "mcl_core:paper", "mcl_core:paper"}
	}
})

-- Give map book to new players
minetest.register_on_newplayer(function(player)
	local inv = player:get_inventory()
	if inv:room_for_item("main", "discovery_maps:map_book") then
		inv:add_item("main", "discovery_maps:map_book")
		minetest.chat_send_player(player:get_player_name(),
			S("Welcome! You've been given a Map Book. left-click it to explore the world!"))
	end
end)

dofile(modpath .. "/marker-gui.lua")
dofile(modpath .. "/marker-share.lua")
dofile(modpath .. "/map-party.lua")
dofile(modpath .. "/map-party-gui.lua")
minetest.log("action", "[persistent_map] Persistent map mod loaded successfully")