extends Node

const SYNCED_STATS: Array = [
  "health", "shield", "max_health", "max_shield",
  "role", "level", "xp", "xp_to_next",
  "buff_damage", "buff_heal", "buff_shield",
]
const SYNC_INTERVAL: float = 0.10

var _entities: Dictionary = {}
var _player_cache: Dictionary = {}
var _dirty: Dictionary = {}
var _accum: float = 0.0

func _ready() -> void:
  set_process(true)

func _process(delta: float) -> void:
  _accum += delta
  if _accum < SYNC_INTERVAL:
    return
  _accum = 0.0
  if multiplayer.multiplayer_peer == null or multiplayer.multiplayer_peer is OfflineMultiplayerPeer or not multiplayer.is_server():
    _dirty.clear()
    return
  if _dirty.is_empty():
    return
  for entity in _dirty.keys():
    if not is_instance_valid(entity) or not entity.is_inside_tree():
      continue
    _sync_stats_batch.rpc(String(entity.get_path()), _dirty[entity])
  _dirty.clear()

@rpc("authority", "unreliable_ordered")
func _sync_stats_batch(path_str: String, changes: Dictionary) -> void:
  var entity := get_node_or_null(NodePath(path_str))
  if entity == null or not entity in _entities:
    return
  for stat in changes.keys():
    _entities[entity][stat] = changes[stat]
    match stat:
      "health":
        EventBus.health_changed.emit(entity, changes[stat], _entities[entity].get("max_health", 100.0))
      "shield":
        EventBus.shield_changed.emit(entity, changes[stat], _entities[entity].get("max_shield", 0.0))
      "role":
        EventBus.role_changed.emit(entity, changes[stat])
      "level":
        EventBus.level_up.emit(entity, changes[stat])
      "buff_damage", "buff_heal", "buff_shield":
        EventBus.buff_changed.emit(entity, StringName(stat), changes[stat])

func register(entity: Node, base_resource: Resource) -> void:
  if not is_instance_valid(entity):
    return
  var data: Dictionary = {}
  for prop in base_resource.get_property_list():
    var name: String = prop.name
    if not (prop.usage & PROPERTY_USAGE_STORAGE):
      continue
    if name in ["resource_local_to_scene", "resource_path", "resource_name", "resource_scene_unique_id", "script"]:
      continue
    data[name] = base_resource.get(name)
  data["health"] = data.get("max_health", 100.0)
  data["shield"] = data.get("max_shield", 0.0)
  data["shield_regen_timer"] = 0.0
  data["base_resource"] = base_resource
  _entities[entity] = data
  EventBus.entity_registered.emit(entity)

func deregister(entity: Node) -> void:
  if entity in _entities:
    _entities.erase(entity)
    EventBus.entity_deregistered.emit(entity)

func has(entity: Node) -> bool:
  return entity in _entities

func get_stat(entity: Node, stat: String, default: Variant = 0.0) -> Variant:
  if entity in _entities:
    return _entities[entity].get(stat, default)
  return default

func set_stat(entity: Node, stat: String, value: Variant) -> void:
  if not entity in _entities:
    return
  var prev: Variant = _entities[entity].get(stat)
  _entities[entity][stat] = value
  if stat in SYNCED_STATS and prev != value and multiplayer.multiplayer_peer != null and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer) and multiplayer.is_server() and is_instance_valid(entity) and entity.is_inside_tree():
    if not entity in _dirty:
      _dirty[entity] = {}
    _dirty[entity][stat] = value


func get_all(entity: Node) -> Dictionary:
  return _entities.get(entity, {})

func entities() -> Array:
  return _entities.keys()

func entities_in_group(group: StringName) -> Array:
  var out: Array = []
  for e in _entities.keys():
    if is_instance_valid(e) and e.is_in_group(group):
      out.append(e)
  return out

func cache_player(peer_id: int, entity: Node) -> void:
  if entity in _entities:
    _player_cache[peer_id] = _entities[entity].duplicate(true)

func restore_player(peer_id: int, entity: Node) -> void:
  if peer_id in _player_cache:
    _entities[entity] = _player_cache[peer_id].duplicate(true)

func clear_player_cache(peer_id: int = -1) -> void:
  if peer_id == -1:
    _player_cache.clear()
  else:
    _player_cache.erase(peer_id)

func clear_all() -> void:
  _entities.clear()
  _player_cache.clear()
