extends Node2D


const _ITEM_BOX_SCENE = preload("res://src/lib/item_box.tscn")
const _ItemBox = preload("res://src/lib/item_box.gd")
const _TYPEWRITER_SEC_PER_CHAR = 0.015
const _TYPEWRITER_SEC_PER_STOP = 0.3

@export var _fade_rect: ColorRect

@export_group("Main Menu")
@export var _main_menu: Control
@export var _offer_container: HFlowContainer
@export var _desired_container: HFlowContainer
@export var _player_inventory_container: HFlowContainer
@export var _dialogue_name_label: RichTextLabel
@export var _dialogue_label: RichTextLabel

@export_group("Offering Menu")
@export var _offering_menu: Control

@export_subgroup("Dialogue")
@export var _offer_dialogue_name_label: RichTextLabel
@export var _offer_dialogue_label: RichTextLabel

@export_subgroup("Player Inventory")
@export var _offer_player_inventory_label: RichTextLabel
@export var _offer_player_inventory_container: HFlowContainer

@export_subgroup("Currently Offered")
@export var _offer_current_items: HFlowContainer

@export_subgroup("Selected Item")
@export var _offer_selected_box: Control
@export var _offer_selected_num_spin_box: SpinBox

@export_subgroup("Item To Add")
@export var _offer_box: Control
@export var _offer_box_slot: Control
@export var _offer_num_spin_box: SpinBox

var _npc: NPC
var _typewriter_timer := 0.0
var _typewriter_wait := 0.0
var _typewriter_running := false

var _offer_temp_inventory: Array[Item]
var _offer_current: Array[Item]
var _offer_selected: Item
var _offer_adding: Item
var _offer_adding_box: Control


func _ready() -> void:
	_fade_rect.show()
	create_tween().tween_property(_fade_rect, "color:a", 0, 0.2)
	
	_npc = GameState.fetch().trade_npc_data.new()
	_dialogue_name_label.text = _npc.npc_name
	_offer_dialogue_name_label.text = _npc.npc_name
	_offer_num_spin_box.get_line_edit().context_menu_enabled = false
	_offer_num_spin_box.get_line_edit().selecting_enabled = false
	_offer_selected_num_spin_box.get_line_edit().context_menu_enabled = false
	_offer_selected_num_spin_box.get_line_edit().selecting_enabled = false
	
	_update_all()
	
	_main_menu.show()
	_offering_menu.hide()
	_typewriter_start(_dialogue_label, _npc.dialogue_beginning().string)


func _process(delta: float) -> void:
	if _typewriter_running:
		_typewriter_timer += delta
	
	if _main_menu.visible:
		_typewriter_label(_dialogue_label)
	elif _offering_menu.visible:
		_typewriter_label(_offer_dialogue_label)


func end_trade() -> void:
	var fade_tween := create_tween()
	fade_tween.tween_property(_fade_rect, "color", Color.BLACK, 0.2)
	await fade_tween.finished
	await get_tree().create_timer(0.1).timeout
	MainEvents.scene_change_requested.emit("res://src/overworld/overworld.tscn")


func _update_offers() -> void:
	var old_children := _offer_container.get_children()
	for child in old_children:
		child.queue_free()
	
	for item in _npc.inventory:
		var new_node: _ItemBox = _ITEM_BOX_SCENE.instantiate()
		new_node.update_from_item(item, true)
		new_node.gui_input.connect(_on_offer_gui_input.bind(item))
		_offer_container.add_child(new_node)


func _update_desired() -> void:
	var old_children := _desired_container.get_children()
	for child in old_children:
		child.queue_free()
	
	for item in _npc.desired:
		var new_node: _ItemBox = _ITEM_BOX_SCENE.instantiate()
		new_node.update_from_item(item, false)
		_desired_container.add_child(new_node)


func _update_inventory() -> void:
	var old_children := _player_inventory_container.get_children()
	for child in old_children:
		child.queue_free()
	
	for item in GameState.fetch().player_inventory.items:
		var new_node: _ItemBox = _ITEM_BOX_SCENE.instantiate()
		new_node.update_from_item(item, true)
		_player_inventory_container.add_child(new_node)


func _update_all() -> void:
	_update_offers()
	_update_desired()
	_update_inventory()


func _offer_update_inventory() -> void:
	var old_children := _offer_player_inventory_container.get_children()
	for child in old_children:
		child.queue_free()
	
	for item in _offer_temp_inventory:
		var new_node: _ItemBox = _ITEM_BOX_SCENE.instantiate()
		new_node.update_from_item(item, true)
		
		new_node.gui_input.connect(_on_offer_inventory_gui_input.bind(item, new_node))
		
		_offer_player_inventory_container.add_child(new_node)


func _offer_update_current() -> void:
	var old_children := _offer_current_items.get_children()
	for child in old_children:
		child.queue_free()
	
	for item in _offer_current:
		var new_node: _ItemBox = _ITEM_BOX_SCENE.instantiate()
		new_node.update_from_item(item, true)
		_offer_current_items.add_child(new_node)


func _offer_update_selected() -> void:
	var old_children := _offer_selected_box.get_children()
	for child in old_children:
		child.queue_free()
	
	# can't get more than what there is
	_offer_selected_num_spin_box.max_value = _offer_selected.amount
	
	# only allow editing/focusing spin box if item is stackable
	_offer_selected_num_spin_box.editable = _offer_selected.stackable
	if _offer_selected.stackable:
		_offer_selected_num_spin_box.focus_mode = Control.FOCUS_ALL
	else:
		_offer_selected_num_spin_box.focus_mode = Control.FOCUS_NONE
	
	var new_node: _ItemBox = _ITEM_BOX_SCENE.instantiate()
	new_node.update_from_item(_offer_selected, true)
	_offer_selected_box.add_child(new_node)
	new_node.set_anchors_and_offsets_preset(Control.PRESET_CENTER)


func _offer_update_adding() -> void:
	var old_children := _offer_box_slot.get_children()
	for child in old_children:
		child.queue_free()
	
	if not is_instance_valid(_offer_adding):
		_offer_num_spin_box.max_value = 1
		_offer_num_spin_box.editable = false
		_offer_num_spin_box.focus_mode = Control.FOCUS_NONE
		return
	
	# can't add more than what you have
	_offer_num_spin_box.max_value = _offer_adding.amount
	
	# only allow editing/focusing spin box if item is stackable
	_offer_num_spin_box.editable = _offer_adding.stackable
	if _offer_adding.stackable:
		_offer_num_spin_box.focus_mode = Control.FOCUS_ALL
	else:
		_offer_num_spin_box.focus_mode = Control.FOCUS_NONE
	
	var new_node: _ItemBox = _ITEM_BOX_SCENE.instantiate()
	new_node.update_from_item(_offer_adding, true)
	_offer_box_slot.add_child(new_node)
	new_node.set_anchors_and_offsets_preset(Control.PRESET_CENTER)


func _offer_update_all() -> void:
	_offer_update_inventory()
	_offer_update_current()
	_offer_update_selected()
	_offer_update_adding()


func _typewriter_start(label: RichTextLabel, text: String) -> void:
	label.text = text
	label.visible_characters = 0
	_typewriter_running = true
	_typewriter_timer = 0.0
	_typewriter_wait = _TYPEWRITER_SEC_PER_CHAR


func _typewriter_label(label: RichTextLabel) -> void:
	# still running
	if _typewriter_timer < _typewriter_wait:
		return
	
	label.visible_characters += 1
	
	# text is fully typed out
	if label.visible_characters == label.text.length():
		_typewriter_running = false
	
	# if the character that was just revealed is punctuation, wait longer
	match label.text[label.visible_characters - 1]:
		"?", "!", ",", ".", ":", ";":
			_typewriter_wait = _TYPEWRITER_SEC_PER_STOP
		_:
			_typewriter_wait = _TYPEWRITER_SEC_PER_CHAR
	
	_typewriter_timer = 0.0


func _offer_go_back() -> void:
	_offer_current.clear()
	
	_main_menu.show()
	_offering_menu.hide()
	_update_all()


func _sort_amount_ascending(a: Item, b: Item) -> bool:
	return a.amount < b.amount


func _sort_amount_descending(a: Item, b: Item) -> bool:
	return a.amount > b.amount


func _item_weight_sum(accum: int, item: Item) -> int:
	return accum + item.weight


func _filter_same_items(item: Item, original: Item) -> bool:
	return item.id == original.id


func _on_offer_gui_input(event: InputEvent, offer: Item) -> void:
	if event is InputEventMouseButton:
		var e := event as InputEventMouseButton
		if e.button_index != MOUSE_BUTTON_LEFT or not e.is_released():
			return
	else:
		return
	
	_main_menu.hide()
	_offering_menu.show()
	
	_offer_selected = offer
	
	# ask for everything by default
	_offer_selected_num_spin_box.max_value = _offer_selected.amount
	_offer_selected_num_spin_box.value = _offer_selected.amount
	
	_offer_temp_inventory = GameState.fetch().player_inventory.items.duplicate()
	_offer_update_all()
	_typewriter_start(_offer_dialogue_label, _npc.dialogue_offer(offer).string)


func _on_offer_inventory_gui_input(event: InputEvent, offer: Item, offer_box: Control) -> void:
	if event is InputEventMouseButton:
		var e := event as InputEventMouseButton
		if e.is_released() and e.button_index == MOUSE_BUTTON_LEFT:
			_offer_adding = offer
			_offer_adding_box = offer_box
			_offer_num_spin_box.max_value = _offer_adding.amount
			_offer_num_spin_box.value = _offer_adding.amount
			_offer_update_adding()


func _on_offer_accept_pressed() -> void:
	# first make sure it would be possible to carry the selected item
	# after giving up the offered items
	var weight: int = _offer_temp_inventory.reduce(_item_weight_sum, 0)
	if (weight + (_offer_selected.weight * _offer_selected.amount)) > GameState.fetch().player_inventory.max_weight:
		var dialog = AcceptDialog.new()
		dialog.dialog_text = "You wouldn't be able to carry all this!"
		dialog.title = ""
		dialog.close_requested.connect(dialog.queue_free)
		get_tree().root.add_child(dialog)
		dialog.popup_centered()
		return
	
	var result := _npc.dialogue_judge(_offer_current)
	if result.value == false: # deal rejected
		_typewriter_start(_offer_dialogue_label, result.string)
	else: # deal accepted
		# you lose what you offered
		GameState.fetch().player_inventory.items = _offer_temp_inventory
		# and get what you wanted
		var obtained: Item = _offer_selected.duplicate()
		obtained.amount = _offer_selected_num_spin_box.value
		GameState.fetch().player_inventory.add_item(obtained)
		
		_typewriter_start(_dialogue_label, result.string)
		_offer_go_back()


func _on_offer_back_pressed() -> void:
	_offer_go_back()


func _on_offer_add_pressed() -> void:
	if not is_instance_valid(_offer_adding):
		return
	
	var added: Item = _offer_adding.duplicate()
	added.amount = _offer_num_spin_box.value
	_offer_current.push_back(added)
	_offer_update_current()
	
	if not _offer_adding.stackable or _offer_adding.amount <= _offer_num_spin_box.value:
		_offer_temp_inventory.erase(_offer_adding)
	else:
		_offer_adding.amount -= _offer_num_spin_box.value
	_offer_update_inventory()
	
	_offer_adding = null
	_offer_update_adding()
