extends CanvasLayer

@onready var hp_bar: ProgressBar = %HpBar
@onready var hp_label: Label = %HpLabel
@onready var shield_bar: ProgressBar = %ShieldBar
@onready var shield_label: Label = %ShieldLabel
@onready var xp_bar: ProgressBar = %XpBar
@onready var level_label: Label = %LevelLabel
@onready var wave_label: Label = %WaveLabel
@onready var timer_label: Label = %WaveTimer
@onready var village_bar: ProgressBar = %VillageBar
@onready var role_icon: Panel = %RoleIcon
@onready var role_label: Label = %RoleLabel
@onready var ability_box: HBoxContainer = %AbilityBox
@onready var death_overlay: Control = %DeathOverlay
@onready var death_label: Label = %DeathLabel
@onready var chat_log: RichTextLabel = %ChatLog
@onready var chat_input: LineEdit = %ChatInput
@onready var inventory_panel: Control = %InventoryPanel
@onready var inventory_list: VBoxContainer = %InventoryList
@onready var crafting_panel: Control = %CraftingPanel
@onready var crafting_list: VBoxContainer = %CraftingList
@onready var build_panel: Control = %BuildPanel
@onready var build_list: HBoxContainer = %BuildList
@onready var dialog_panel: Control = %DialogPanel
@onready var dialog_npc: Label = %DialogNpc
@onready var dialog_log: RichTextLabel = %DialogLog
@onready var dialog_input: LineEdit = %DialogInput
@onready var map_panel: Control = %MapPanel
@onready var map_canvas: Control = %MapCanvas
@onready var minimap_canvas: Control = %MinimapCanvas
@onready var pause_panel: Control = %PausePanel
@onready var game_over_overlay: Control = %GameOverOverlay
@onready var ability_buttons: Array = []

var local_player: Node = null
var dialog_npc_node: Node = null
var build_selected: int = 0
var build_rotation: float = 0.0
var build_preview: MeshInstance3D = null
var build_active: bool = false
var _minimap_accum: float = 0.0

func _ready() -> void:
  add_to_group("dialog_ui")
  EventBus.health_changed.connect(_on_health_changed)
  EventBus.shield_changed.connect(_on_shield_changed)
  EventBus.cooldown_tick.connect(_on_cooldown_tick)
  EventBus.role_changed.connect(_on_role_changed)
  EventBus.entity_died.connect(_on_entity_died)
  EventBus.entity_respawned.connect(_on_respawned)
  EventBus.wave_started.connect(_on_wave_started)
  EventBus.wave_timer_tick.connect(_on_wave_tick)
  EventBus.village_damaged.connect(_on_village_damaged)
  EventBus.village_destroyed.connect(_on_village_destroyed)
  EventBus.invasion_started.connect(_on_invasion_started)
  EventBus.xp_gained.connect(_on_xp_gained)
  EventBus.level_up.connect(_on_level_up)
  EventBus.dialog_opened.connect(_on_dialog_opened)
  EventBus.inventory_changed.connect(_on_inventory_changed)
  EventBus.chat_message.connect(_on_chat)
  chat_input.text_submitted.connect(_on_chat_submitted)
  dialog_input.text_submitted.connect(_on_dialog_submitted)
  death_overlay.visible = false
  inventory_panel.visible = false
  crafting_panel.visible = false
  build_panel.visible = false
  dialog_panel.visible = false
  map_panel.visible = false
  pause_panel.visible = false
  game_over_overlay.visible = false
  set_process(true)
  _wire_ability_buttons()
  call_deferred("_populate_build_list")

func _process(_delta: float) -> void:
  if local_player == null:
    local_player = _find_local_player()
    if local_player:
      var role: int = int(Stats.get_stat(local_player, "role", GameState.ROLE_DAMAGE))
      _on_role_changed(local_player, role)
      _refresh_vitals()
  if build_active:
    _update_build_preview()
  _update_minimap()

func _unhandled_input(event: InputEvent) -> void:
  if event.is_action_pressed("inventory"):
    _toggle_panel(inventory_panel)
    _refresh_inventory()
  elif event.is_action_pressed("crafting"):
    _toggle_panel(crafting_panel)
    _refresh_crafting()
  elif event.is_action_pressed("build_mode"):
    _toggle_build_mode()
  elif event.is_action_pressed("map"):
    _toggle_panel(map_panel)
  elif event.is_action_pressed("chat"):
    chat_input.grab_focus()
    _capture_ui(true)
  elif event.is_action_pressed("pause"):
    if dialog_panel.visible:
      dialog_panel.visible = false
      _capture_ui(false)
    elif build_active:
      _toggle_build_mode()
    elif inventory_panel.visible or crafting_panel.visible or map_panel.visible:
      inventory_panel.visible = false
      crafting_panel.visible = false
      map_panel.visible = false
      _capture_ui(false)
    else:
      _toggle_pause()
  elif build_active:
    if event.is_action_pressed("rotate_build"):
      build_rotation = wrapf(build_rotation + PI * 0.5, 0.0, TAU)
    elif event.is_action_pressed("ability_1"):
      _select_build(0)
    elif event.is_action_pressed("ability_2"):
      _select_build(1)
    elif event.is_action_pressed("ability_3"):
      _select_build(2)
    elif event.is_action_pressed("ability_4"):
      _select_build(3)
    elif event is InputEventMouseButton and event.pressed:
      if event.button_index == MOUSE_BUTTON_LEFT:
        _try_place()
      elif event.button_index == MOUSE_BUTTON_MIDDLE:
        _try_remove()

func _wire_ability_buttons() -> void:
  for c in ability_box.get_children():
    if c is Button:
      ability_buttons.append(c)

func _toggle_panel(panel: Control) -> void:
  panel.visible = not panel.visible
  _capture_ui(_any_panel_visible())

func _any_panel_visible() -> bool:
  return inventory_panel.visible or crafting_panel.visible or dialog_panel.visible or pause_panel.visible

func _capture_ui(v: bool) -> void:
  if local_player and local_player.has_method("set_ui_capturing"):
    local_player.set_ui_capturing(v)

func _find_local_player() -> Node:
  for p in get_tree().get_nodes_in_group("player"):
    if p.is_multiplayer_authority():
      return p
  return null

func _refresh_vitals() -> void:
  if local_player == null:
    return
  var hp: float = float(Stats.get_stat(local_player, "health", 0.0))
  var max_hp: float = float(Stats.get_stat(local_player, "max_health", 1.0))
  _on_health_changed(local_player, hp, max_hp)
  var shield: float = float(Stats.get_stat(local_player, "shield", 0.0))
  var max_shield: float = float(Stats.get_stat(local_player, "max_shield", 0.0))
  _on_shield_changed(local_player, shield, max_shield)
  var xp: float = float(Stats.get_stat(local_player, "xp", 0.0))
  var to_next: float = float(Stats.get_stat(local_player, "xp_to_next", 50.0))
  xp_bar.max_value = to_next
  xp_bar.value = xp
  level_label.text = "Lv %d" % int(Stats.get_stat(local_player, "level", 1))

func _on_health_changed(entity: Node, current: float, max: float) -> void:
  if entity != local_player:
    return
  hp_bar.max_value = max
  hp_bar.value = current
  hp_label.text = "%d / %d" % [int(current), int(max)]

func _on_shield_changed(entity: Node, current: float, max: float) -> void:
  if entity != local_player:
    return
  shield_bar.max_value = max if max > 0 else 1
  shield_bar.value = current
  shield_label.text = "%d / %d" % [int(current), int(max)]

func _on_cooldown_tick(entity: Node, cds: PackedFloat32Array, _max_cds: PackedFloat32Array, gcd: float) -> void:
  if entity != local_player:
    return
  for i in range(min(ability_buttons.size(), cds.size())):
    var btn: Button = ability_buttons[i]
    if cds[i] > 0.0:
      btn.text = "%d\n%.1f" % [i + 1, cds[i]]
      btn.disabled = true
    elif gcd > 0.0:
      btn.text = "%d\nGCD" % (i + 1)
      btn.disabled = true
    else:
      btn.text = "%d" % (i + 1)
      btn.disabled = false

func _on_role_changed(player: Node, role: int) -> void:
  if player != local_player and player != _find_local_player():
    return
  if local_player == null:
    local_player = player
  match role:
    GameState.ROLE_TANK:
      role_label.text = "T"
      role_icon.modulate = Color(0.3, 0.5, 0.95)
    GameState.ROLE_DAMAGE:
      role_label.text = "D"
      role_icon.modulate = Color(0.95, 0.3, 0.3)
    GameState.ROLE_HEALER:
      role_label.text = "H"
      role_icon.modulate = Color(0.4, 0.85, 0.4)

func _on_entity_died(entity: Node) -> void:
  if entity != local_player:
    return
  death_overlay.visible = true
  death_label.text = "Respawning..."

func _on_respawned(entity: Node) -> void:
  if entity != local_player:
    return
  death_overlay.visible = false

func _on_wave_started(wave: int) -> void:
  wave_label.text = "Wave %d" % wave

func _on_wave_tick(seconds: float) -> void:
  var m := int(seconds) / 60
  var s := int(seconds) % 60
  timer_label.text = "%02d:%02d" % [m, s]

func _on_village_damaged(current: float, max: float) -> void:
  village_bar.max_value = max
  village_bar.value = current

func _on_village_destroyed() -> void:
  game_over_overlay.visible = true

func _on_invasion_started() -> void:
  timer_label.modulate = Color(1.0, 0.4, 0.3)

func _on_xp_gained(player: Node, _amount: float) -> void:
  if player != local_player:
    return
  var xp: float = float(Stats.get_stat(player, "xp", 0.0))
  var to_next: float = float(Stats.get_stat(player, "xp_to_next", 50.0))
  xp_bar.max_value = to_next
  xp_bar.value = xp

func _on_level_up(player: Node, new_level: int) -> void:
  if player != local_player:
    return
  level_label.text = "Lv %d" % new_level
  _refresh_vitals()

func _on_dialog_opened(player: Node, npc: Node) -> void:
  if player != local_player:
    return
  dialog_panel.visible = true
  dialog_npc_node = npc
  dialog_npc.text = npc.profile.display_name
  dialog_log.text = "[i]" + npc.profile.greeting + "[/i]\n"
  dialog_input.text = ""
  dialog_input.grab_focus()
  _capture_ui(true)

func _on_dialog_submitted(text: String) -> void:
  if dialog_npc_node == null:
    return
  var dialog_sys: Node = _find_system("DialogSystem")
  if dialog_sys == null:
    return
  dialog_log.append_text("[b]Du:[/b] " + text + "\n[i]...[/i]\n")
  dialog_input.text = ""
  dialog_sys.ask(dialog_npc_node, local_player, text)

func show_answer(text: String) -> void:
  if dialog_npc_node == null:
    return
  dialog_log.text = dialog_log.text.replace("[i]...[/i]\n", "")
  dialog_log.append_text("[b]" + dialog_npc_node.profile.display_name + ":[/b] " + text + "\n")

func _on_inventory_changed(player: Node) -> void:
  if player != local_player:
    return
  _refresh_inventory()

func _refresh_inventory() -> void:
  for c in inventory_list.get_children():
    c.queue_free()
  if local_player == null:
    return
  var inv_sys: Node = _find_system("InventorySystem")
  if inv_sys == null:
    return
  var inv: Dictionary = inv_sys.get_inventory(local_player)
  if inv.is_empty():
    var lbl := Label.new()
    lbl.text = "(empty)"
    inventory_list.add_child(lbl)
    return
  for k in inv.keys():
    var lbl := Label.new()
    lbl.text = "%s: %d" % [str(k), inv[k]]
    inventory_list.add_child(lbl)

func _refresh_crafting() -> void:
  for c in crafting_list.get_children():
    c.queue_free()
  if local_player == null:
    return
  var c_sys: Node = _find_system("CraftingSystem")
  var inv_sys: Node = _find_system("InventorySystem")
  if c_sys == null or inv_sys == null:
    return
  for r in c_sys.get_recipes():
    var btn := Button.new()
    var inputs_str: String = ""
    for k in r.inputs.keys():
      inputs_str += "%s x%d  " % [str(k), r.inputs[k]]
    btn.text = "%s   (%s)" % [r.name, inputs_str.strip_edges()]
    btn.disabled = not c_sys.can_craft(local_player, r)
    btn.pressed.connect(func(): c_sys.craft(local_player, r.id); _refresh_crafting())
    crafting_list.add_child(btn)

func _populate_build_list() -> void:
  for c in build_list.get_children():
    c.queue_free()
  var b_sys: Node = _find_system("BuildingSystem")
  if b_sys == null:
    return
  var bps: Array = b_sys.get_blueprints()
  for i in range(bps.size()):
    var btn := Button.new()
    btn.text = "%d %s\n%s x%d" % [i + 1, bps[i].name, str(bps[i].material), bps[i].cost]
    btn.toggle_mode = true
    btn.button_pressed = (i == 0)
    btn.pressed.connect(func(): _select_build(i))
    build_list.add_child(btn)

func _select_build(idx: int) -> void:
  build_selected = idx
  var btns := build_list.get_children()
  for i in range(btns.size()):
    if btns[i] is Button:
      (btns[i] as Button).button_pressed = (i == idx)
  if build_preview:
    _update_preview_mesh()

func _toggle_build_mode() -> void:
  build_active = not build_active
  build_panel.visible = build_active
  if local_player and local_player.has_method("set_build_mode"):
    local_player.set_build_mode(build_active)
  if build_active:
    _create_build_preview()
  elif build_preview:
    build_preview.queue_free()
    build_preview = null

func _create_build_preview() -> void:
  if build_preview:
    build_preview.queue_free()
  var world: Node = get_tree().current_scene
  if world == null:
    return
  build_preview = MeshInstance3D.new()
  build_preview.cast_shadow = MeshInstance3D.SHADOW_CASTING_SETTING_OFF
  world.add_child(build_preview)
  _update_preview_mesh()

func _update_preview_mesh() -> void:
  if build_preview == null:
    return
  var b_sys: Node = _find_system("BuildingSystem")
  if b_sys == null:
    return
  var bp: Dictionary = b_sys.get_blueprints()[build_selected]
  var box := BoxMesh.new()
  box.size = bp.size
  build_preview.mesh = box
  var mat := StandardMaterial3D.new()
  var c: Color = bp.color
  c.a = 0.5
  mat.albedo_color = c
  mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
  mat.flags_unshaded = true
  build_preview.material_override = mat

func _update_build_preview() -> void:
  if build_preview == null or local_player == null:
    return
  var b_sys: Node = _find_system("BuildingSystem")
  if b_sys == null:
    return
  var pos: Vector3 = _ground_under_cursor()
  var snapped: Vector3 = b_sys.snap_position(pos)
  var bp: Dictionary = b_sys.get_blueprints()[build_selected]
  build_preview.global_position = snapped + Vector3(0, bp.size.y * 0.5, 0)
  build_preview.rotation.y = build_rotation

func _ground_under_cursor() -> Vector3:
  var cam: Camera3D = local_player.camera if local_player.has_method("get") else null
  if cam == null:
    return Vector3.ZERO
  var mouse := get_viewport().get_mouse_position()
  var from := cam.project_ray_origin(mouse)
  var dir := cam.project_ray_normal(mouse)
  if abs(dir.y) < 0.001:
    return from
  var t: float = -from.y / dir.y
  if t <= 0:
    return from
  return from + dir * t

func _try_place() -> void:
  if local_player == null:
    return
  var b_sys: Node = _find_system("BuildingSystem")
  if b_sys == null:
    return
  var bps: Array = b_sys.get_blueprints()
  var bp: Dictionary = bps[build_selected]
  var pos: Vector3 = _ground_under_cursor()
  b_sys.place(local_player, bp.id, pos, build_rotation)

func _try_remove() -> void:
  if local_player == null:
    return
  var cam: Camera3D = local_player.camera if local_player.has_method("get") else null
  if cam == null:
    return
  var mouse := get_viewport().get_mouse_position()
  var from := cam.project_ray_origin(mouse)
  var to := from + cam.project_ray_normal(mouse) * 100.0
  var space: PhysicsDirectSpaceState3D = local_player.get_world_3d().direct_space_state
  var query := PhysicsRayQueryParameters3D.create(from, to)
  query.collision_mask = 16
  var hit: Dictionary = space.intersect_ray(query)
  if hit.is_empty():
    return
  var node: Node = hit.collider
  while node and not node.is_in_group("buildings"):
    node = node.get_parent()
  if node:
    var b_sys: Node = _find_system("BuildingSystem")
    if b_sys:
      b_sys.remove(local_player, node.get_path())

func _on_chat(_peer_id: int, sender: String, text: String) -> void:
  chat_log.append_text("[b]%s:[/b] %s\n" % [sender, text])

func _on_chat_submitted(text: String) -> void:
  var c_sys: Node = _find_system("ChatSystem")
  if c_sys:
    c_sys.send(text)
  chat_input.text = ""
  chat_input.release_focus()
  _capture_ui(_any_panel_visible())

func _toggle_pause() -> void:
  pause_panel.visible = not pause_panel.visible
  if pause_panel.visible and multiplayer.multiplayer_peer is OfflineMultiplayerPeer:
    GameState.set_paused(true)
  else:
    GameState.set_paused(false)
  _capture_ui(_any_panel_visible())

func _on_resume_pressed() -> void:
  pause_panel.visible = false
  GameState.set_paused(false)
  _capture_ui(_any_panel_visible())

func _on_quit_pressed() -> void:
  Net.disconnect_net()
  GameState.set_paused(false)
  GameState.change_scene(GameState.SCENE_MAIN_MENU)

func _on_game_over_restart() -> void:
  Net.disconnect_net()
  GameState.set_paused(false)
  GameState.change_scene(GameState.SCENE_MAIN_MENU)

func _update_minimap() -> void:
  _minimap_accum += get_process_delta_time()
  if _minimap_accum < 0.20:
    return
  _minimap_accum = 0.0
  minimap_canvas.queue_redraw()
  if map_panel.visible:
    map_canvas.queue_redraw()

func _find_system(name: String) -> Node:
  var n: Node = get_tree().root.get_node_or_null("World/Systems/" + name)
  if n == null:
    n = get_tree().root.get_node_or_null("Dungeon/Systems/" + name)
  return n
