## WorkshopBrowser — Server-Side Map Directory Scanner
##
## Singleton (or child of ServerMain) that scans the server's maps
## directory for .pck files and .tscn references, maintaining a
## Dictionary of available maps.
##
## ## Map Directory Resolution
##
##   Default:   user://maps/  (resolves via ProjectSettings.globalize_path)
##   Override:  MAP_DIR env var (absolute path, for container/VPS deployments)
##
##   The browser scans for `*.pck` files and reads their file metadata.
##   For built-in (non-PCK) maps, it checks `res://scenes/map/<name>.tscn`.
##
## ## Map Info Dictionary Shape
##
##   {
##       "name": String,
##       "description": String,
##       "file_size": int,
##       "map_type": String,       # "pck" | "builtin"
##       "player_count": int,      # inferred or 0 (reserved for future metadata)
##       "path": String,           # absolute path to the .pck or .tscn
##   }
##
## ## Signals
##
##   maps_refreshed(map_list: Array) — emitted after refresh_maps() completes
##
## =============================================================================

extends Node

# ---------------------------------------------------------------------------
# Signals
# ---------------------------------------------------------------------------

## Emitted after refresh_maps() completes.
signal maps_refreshed(map_list: Array)

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

## Default maps directory (relative to user://).
const DEFAULT_MAPS_DIR: String = "user://maps/"

## Path to built-in map scenes (non-PCK).
const BUILTIN_MAP_DIR: String = "res://scenes/map/"

# ---------------------------------------------------------------------------
# State
# ---------------------------------------------------------------------------

## Available maps keyed by map_id (String → Dictionary).
var _available_maps: Dictionary = {}

## Absolute path to the maps directory being scanned.
var _maps_dir: String = ""

# ---------------------------------------------------------------------------
# Lifecycle / Initialisation
# ---------------------------------------------------------------------------

func _ready() -> void:
	_resolve_maps_dir()
	print("[WorkshopBrowser] Maps directory: %s" % _maps_dir)
	call_deferred(&"refresh_maps")

## Resolve the maps directory — env var override, then user:// default.
func _resolve_maps_dir() -> void:
	if OS.has_environment("MAP_DIR"):
		_maps_dir = OS.get_environment("MAP_DIR")
		if not _maps_dir.ends_with("/"):
			_maps_dir += "/"
		return

	_maps_dir = ProjectSettings.globalize_path(DEFAULT_MAPS_DIR)
	if not _maps_dir.ends_with("/"):
		_maps_dir += "/"

# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

## Rescan the maps directory and rebuild the available map list.
## Returns the Array of map info Dictionaries (same as get_map_list()).
func refresh_maps() -> Array:
	_available_maps.clear()
	_scan_pck_files()
	_scan_builtin_maps()
	var list: Array = get_map_list()
	maps_refreshed.emit(list)
	print("[WorkshopBrowser] Refreshed maps: %d available" % list.size())
	return list

## Return a serialized list of all available maps as Array[Dictionary].
## Each entry matches the map info shape documented above.
func get_map_list() -> Array:
	var result: Array = []
	for map_id in _available_maps:
		result.append(_available_maps[map_id])
	return result

## Get info for a single map by its map_id (file stem, e.g. "de_dust2").
## Returns an empty Dictionary if the map is not found.
func get_map_info(map_id: String) -> Dictionary:
	return _available_maps.get(map_id, {})

## Check if a map exists in the available list.
func has_map(map_id: String) -> bool:
	return _available_maps.has(map_id)

## Get the total number of available maps.
func get_map_count() -> int:
	return _available_maps.size()

## Get the absolute path of the maps directory being scanned.
func get_maps_directory() -> String:
	return _maps_dir

# ---------------------------------------------------------------------------
# Internal: Scanning
# ---------------------------------------------------------------------------

## Scan for .pck files in the maps directory.
func _scan_pck_files() -> void:
	var dir := DirAccess.open(_maps_dir)
	if dir == null:
		push_warning("[WorkshopBrowser] Cannot open maps directory: %s" % _maps_dir)
		return

	dir.list_dir_begin()
	var file_name: String = dir.get_next()
	while not file_name.is_empty():
		if file_name.ends_with(".pck"):
			_register_pck_file(file_name)
		file_name = dir.get_next()
	dir.list_dir_end()

## Register a single .pck file found during scanning.
func _register_pck_file(file_name: String) -> void:
	var map_id: String = file_name.trim_suffix(".pck")
	var full_path: String = _maps_dir.path_join(file_name)

	var file_size: int = 0
	if FileAccess.file_exists(full_path):
		file_size = FileAccess.get_size(full_path)

	var map_info: Dictionary = {
		name = map_id,
		description = "",           # reserved for future sidecar metadata
		file_size = file_size,
		map_type = "pck",
		player_count = 0,           # inferred or from metadata (future)
		path = full_path,
	}

	_available_maps[map_id] = map_info
	print("[WorkshopBrowser] Registered PCK map: %s (%d bytes)" % [map_id, file_size])

## Scan for built-in .tscn maps (non-PCK, part of the game resources).
func _scan_builtin_maps() -> void:
	# Use DirAccess on res:// to find .tscn files in the map directory.
	# This only works in non-exported builds or with packed resources.
	var res_path: String = BUILTIN_MAP_DIR
	if not ResourceLoader.exists(res_path):
		# res://scenes/map/ might not be a directory; skip silently.
		return

	var dir := DirAccess.open(res_path)
	if dir == null:
		return

	dir.list_dir_begin()
	var file_name: String = dir.get_next()
	while not file_name.is_empty():
		if file_name.ends_with(".tscn"):
			var map_id: String = file_name.trim_suffix(".tscn")
			# Don't override a PCK-registered map
			if _available_maps.has(map_id):
				file_name = dir.get_next()
				continue

			var full_path: String = res_path.path_join(file_name)
			var map_info: Dictionary = {
				name = map_id,
				description = "",
				file_size = 0,
				map_type = "builtin",
				player_count = 0,
				path = full_path,
			}
			_available_maps[map_id] = map_info
			print("[WorkshopBrowser] Registered builtin map: %s" % map_id)
		file_name = dir.get_next()
	dir.list_dir_end()
