extends VBoxContainer

enum RESULTS_STATE {IDLE, NOT_IDLE}

# The time between acting on buffered signals
const SIGNAL_BUFFER_TIMEOUT: float = 0.5

export(PackedScene) var MediaListItem_scene: PackedScene = load("res://scenes/custom_nodes/media_list_item/media_list_item.tscn")
export(PackedScene) var MediaWindow_scene: PackedScene = load("res://scenes/custom_nodes/media_window/media_window.tscn")
export(PackedScene) var ChannelWindow_scene: PackedScene = load("res://scenes/custom_nodes/channel_window/channel_window.tscn")
export(PackedScene) var ChannelCard_scene: PackedScene = load("res://scenes/custom_nodes/channel_card/channel_card.tscn")
export(PackedScene) var ButtonContainer_scene: PackedScene = load("res://scenes/custom_nodes/button_container/button_container.tscn")

var _results_state: int = RESULTS_STATE.IDLE
var _last_claim_search_args: Dictionary = {}
var _last_page_loaded: int = 0
var _last_result_displayed: int = -1
var _exhausted_results: bool = false
var _displayed_exhausted_results: bool = false
var _results: Array = []
# Right margin offset that should be applied so that the scroll bar hugs the
# edge of the window
var _results_scroll_container_offset: int = 0
# Flags that are set to `true` when certain signals have been buffered,
# indicating that the actions the signals should cause should be executed when
# the buffer timer timeouts
var _make_scrollbar_visible_flag: bool = false
var _adjust_results_margin_flag: bool = false
var _is_ready: bool = false
# This timer is used to buffer signals that are emitted too frequently so their
# callbacks can be executed as if only 1 signal was emitted, reducing
# computation. When the timer timeouts, it will execute the callbacks of the
# emitted signals
var _signal_buffer_timer := Timer.new()
var _async_helper := AsyncHelper.new(self)
onready var _results_node: Container = find_node("Results")
onready var _results_scroll_container: ScrollContainer = \
		find_node("ResultsScrollContainer")
onready var _results_margin_container: MarginContainer = \
		find_node("ResultsMarginContainer")
onready var _search_bar: LineEdit = find_node("SearchBar")
onready var _search_hbox_container: HBoxContainer = \
		find_node("SearchHBoxContainer")
onready var _search_options_container: PanelContainer = \
		find_node("SearchOptionsContainer")
onready var _search_options: GridContainer = find_node("SearchOptions")
onready var _search_options_button: Button = find_node("SearchOptionsButton")


func _ready() -> void:
	connect("visibility_changed", self, "_on_visibility_changed")
	_results_node.connect(
			"resized", self, "_on_Results_resized")
	_results_scroll_container.connect(
			"vertical_scroll_ended", self, "_on_Results_vertical_scroll_ended")
	_results_scroll_container.get_v_scrollbar().connect("visibility_changed",
			self, "_on_ResultsScrollContainer_v_scrollbar_visibility_changed")
	_search_bar.connect("text_entered", self, "_on_SearchBar_text_entered")
	_search_options_button.connect(
			"pressed", self, "_on_SearchOptionsButton_pressed")
	_signal_buffer_timer.connect(
			"timeout", self, "_on_signal_buffer_timer_timeout")
	
	_results_node.set_item_size(MediaListItem.SIZE)
	
	_signal_buffer_timer.set_wait_time(SIGNAL_BUFFER_TIMEOUT)
	_signal_buffer_timer.set_autostart(true)
	add_child(_signal_buffer_timer)
	
	# Wait until the lbrynet daemon is ready
	var wait_timer := Timer.new()
	wait_timer.set_one_shot(true)
	add_child(wait_timer)
	var tracker: Dictionary = _async_helper.connect_wrapped(
			SDK.is_ready(), "completed")
	yield(_async_helper.wait_until_finished([tracker]), "completed")
	var sdk_ready: bool = tracker["result"]
	while sdk_ready == false:
		# Don't use `get_tree().create_timer() because the node may have been
		# removed from the tree
		wait_timer.start(1)
		yield(wait_timer, "timeout")
		
		tracker = _async_helper.connect_wrapped(SDK.is_ready(), "completed")
		yield(_async_helper.wait_until_finished([tracker]), "completed")
		sdk_ready = tracker["result"]
	remove_child(wait_timer)
	wait_timer.queue_free()
	
	yield(_set_initial_args(), "completed")
	_make_scrollbar_visible_flag = true
	
	_is_ready = true


func _set_initial_args() -> void:
	"""
	If some channels are followed then sets `_last_claim_search_args` to the
	result of `_get_followed_args()`, otherswise to the results of
	`_get_trending_args`
	"""
	var tracker: Dictionary = _async_helper.connect_wrapped(
			LBRYActions.get_followed_channels(), "completed")
	yield(_async_helper.wait_until_finished([tracker]), "completed")
	var followed_channels: Array = tracker["result"]
	
	var args: Dictionary
	if len(followed_channels) > 0:
		args = yield(_get_followed_args(), "completed")
	else:
		args = _get_trending_args()
	_last_claim_search_args = args


func _add_results(args: Dictionary, ignore_state: bool=false) -> void:
	if _results_state == RESULTS_STATE.IDLE or ignore_state:
		if not ignore_state:
			_results_state = RESULTS_STATE.NOT_IDLE
		
		_results += yield(_get_media(args), "completed")
		_display_media()
		
		if not ignore_state:
			_results_state = RESULTS_STATE.IDLE


func _add_results_until_scrollbar_visible(ignore_state: bool=false) -> void:
	"""
	Ensure the number of items displayed is enough to make the scrollbar
	visible (because dragging the scrollbar to the bottom is what makes more
	items load)
	"""
	# Must have `yield` here so that a `GDScriptFunctionState` is returned on
	# all paths
	var tracker: Dictionary = _async_helper.connect_wrapped(
			get_tree(), "idle_frame", false)
	yield(_async_helper.wait_until_finished([tracker]), "completed")
	if _is_ready and (_results_state == RESULTS_STATE.IDLE or ignore_state):
		if not ignore_state:
			_results_state = RESULTS_STATE.NOT_IDLE
		
		while (
				is_visible_in_tree()
				and not _results_scroll_container \
						.get_v_scrollbar() \
						.is_visible_in_tree()
				and not _displayed_exhausted_results
		):
			yield(_add_results(_last_claim_search_args, true), "completed")
		
		if not ignore_state:
			_results_state = RESULTS_STATE.IDLE


func _apply_search(search_text: String, ignore_state: bool=false) -> void:
	"""
	Performs a search based on the text in the search box and the selected
	search options
	"""
	if _results_state == RESULTS_STATE.IDLE or ignore_state:
		if not ignore_state:
			_results_state = RESULTS_STATE.NOT_IDLE
		
		_clear()
		var args: Dictionary = yield(
				_get_searched_args(search_text), "completed")
		yield(_add_results(args, true), "completed")
		yield(_add_results_until_scrollbar_visible(), "completed")
		
		if not ignore_state:
			_results_state = RESULTS_STATE.IDLE


func _get_trending_args() -> Dictionary:
	var args: Dictionary = {
		"claim_type": "stream",
		"order_by": ["trending_mixed"],
		"no_totals": true,
		"include_is_my_output": true,
		"remove_duplicates": true,
	}
	return args


func _get_followed_args() -> Dictionary:
	var tracker: Dictionary = _async_helper.connect_wrapped(
			LBRYActions.get_followed_channels(), "completed")
	yield(_async_helper.wait_until_finished([tracker]), "completed")
	var followed_channels: Array = tracker["result"]
	var channel_claim_ids: Array = []
	for channel in followed_channels:
		channel_claim_ids.append(channel.rsplit("#", true, 1)[1])
	var args: Dictionary = {
		"claim_type": "stream",
		"channel_ids": channel_claim_ids,
		"order_by": ["trending_mixed"],
		"no_totals": true,
		"include_is_my_output": true,
		"remove_duplicates": true,
	}
	return args


func _get_searched_args(search_text: String) -> Dictionary:
	var tracker: Dictionary = _async_helper.connect_wrapped(
			_search_options.get_options(), "completed")
	yield(_async_helper.wait_until_finished([tracker]), "completed")
	var search_options: Dictionary = tracker["result"]
	var args: Dictionary = {
		"order_by": ["support_amount"],
		"text": search_text,
		"no_totals": true,
		"valid_channel_signature": true,
		"include_is_my_output": true,
		"remove_duplicates": true,
	}
	for key in search_options:
		args[key] = search_options[key]
	return args


func _get_media(args: Dictionary) -> Array:
	"""
	Performs a claim search using the given `args` on the page after the last
	page that was loaded. Also removes any results owned by the user
	"""
	_update_last_args(args)
	var media: Array = []
	# For some reason this yield gets stuck unless a container is resized
	var size_flag: int = _search_hbox_container.get_v_size_flags()
	_search_hbox_container.set_v_size_flags(SIZE_EXPAND)
	var tracker: Dictionary = _async_helper.connect_wrapped(
			get_tree(), "idle_frame", false)
	yield(_async_helper.wait_until_finished([tracker]), "completed")
	_search_hbox_container.set_v_size_flags(size_flag)
	if not _exhausted_results:
		# Merge the permanent search options with the selected options
		var permanent_search_options: Dictionary = Data.get_search_options()
		Helpers.dictionary_merge_into(
				args, permanent_search_options, true, false)
		args["page"] = _last_page_loaded + 1
		
		tracker = _async_helper.connect_wrapped(
				SDK.claim_search(args), "completed")
		yield(_async_helper.wait_until_finished([tracker]), "completed")
		var search_response: Dictionary = tracker["result"]
		media = search_response.get("result", {}).get("items", [])
		
		# Remove media owned by the current account
		for i in range(len(media) - 1, -1, -1):
			if media[i].get("is_my_output", false) == true:
				media.remove(i)
		
		if len(media) == 0:
			_exhausted_results = true
		_last_page_loaded += 1
	return media


func _display_media() -> void:
	var results_slice = \
			_results.slice(_last_result_displayed + 1, len(_results) - 1)
	if len(results_slice) == 0 and not _displayed_exhausted_results:
		var label := Label.new()
		label.set_text("No more results")
		_results_node.add_child(label)
		_displayed_exhausted_results = true
	
	for item in results_slice:
		var item_type: String = item.get("value_type")
		if item_type == "channel":
			var item_node: ButtonContainer = ButtonContainer_scene.instance()
			var channel_card: ChannelCard = ChannelCard_scene.instance()
			var channel_url: String = item.get("short_url", "")
			
			item_node.set_h_size_flags(SIZE_EXPAND_FILL)
			item_node.set_normal_color(Color("3c3c46"))
			item_node.set_hovered_color(Color("50505a"))
			item_node.set_pressed_color(Color("32323c"))
			channel_card.init(channel_url)
			
			if not item_node.is_connected(
					"pressed", self, "_open_channel_window"):
				item_node.connect(
						"pressed", self, "_open_channel_window", [channel_url])
			
			item_node.add_child(channel_card)
			_results_node.add_child(item_node)
			_last_result_displayed += 1
		elif item_type == "stream":
			var item_node: MediaListItem = MediaListItem_scene.instance()
			var channel_url: String = item \
					.get("signing_channel", {}) \
					.get("short_url", "")
			item_node.init(item)
			
			if not item_node.is_media_button_connected(
					"pressed", self, "_open_media_window"):
				item_node.connect_media_button(
						"pressed", self, "_open_media_window", [item])
			if not item_node.is_channel_button_connected(
					"pressed", self, "_open_channel_window"):
				item_node.connect_channel_button(
						"pressed", self, "_open_channel_window", [channel_url])
			
			_results_node.add_child(item_node)
			_last_result_displayed += 1


func _clear() -> void:
	""" Removes all the results """
	for child in _results_node.get_children():
		if child.get_child_count() > 0 and child.get_child(0) is ChannelCard:
			var channel_card: ChannelCard = child.get_child(0)
			child.remove_child(channel_card)
		child.queue_free()
	_results.clear()
	_last_page_loaded = 0
	_last_result_displayed = -1
	_displayed_exhausted_results = false


func _update_last_args(args: Dictionary) -> bool:
	var changed: bool = false
	if args.hash() != _last_claim_search_args.hash():
		_last_page_loaded = 0
		_exhausted_results = false
		changed = true
	_last_claim_search_args = args
	return changed


func _make_scroll_bar_hug_window_edge() -> void:
	"""
	Make the results scrollbar hug the edge of the window to make scrolling
	with the mouse easier
	"""
	if _results_scroll_container.get_v_scrollbar().is_visible_in_tree():
		var scroll_container_right_edge: float = (
				_results_scroll_container.get_global_position().x
				+ _results_scroll_container.get_size().x
		)
		var diff := int(OS.get_window_size().x - scroll_container_right_edge)
		_results_scroll_container_offset = \
				_results_scroll_container_offset - diff
		_results_margin_container.add_constant_override("margin_right",
				_results_scroll_container_offset)


func _apply_margin_if_scrollbar_visible() -> void:
	"""
	Ensures that the content of the ScrollContainer stays the same distance
	from the scrollbar when its visible as it does the edge of the
	ScrollContainer when the scrollbar is invisble
	"""
	if is_visible_in_tree():
		var right_margin: int = (
				_results_scroll_container_offset
				if _results_scroll_container.get_v_scrollbar().is_visible_in_tree()
				else 0
		)
		_results_margin_container.add_constant_override(
				"margin_right", right_margin)


func _open_media_window(data: Dictionary) -> void:
	var media_window: Control = MediaWindow_scene.instance()
	media_window.init(data)
	get_parent().set_window(media_window)


func _open_channel_window(channel_url: String) -> void:
	var channel_window: Control = ChannelWindow_scene.instance()
	channel_window.init(channel_url)
	get_parent().set_window(channel_window)


func _on_visibility_changed() -> void:
	"""
	If the this section (Home) is selected and then the user quickly changes
	tabs then switches back to the previous tab, the content of this section
	will not load. This fixes that
	"""
	_make_scrollbar_visible_flag = true


func _on_Results_resized() -> void:
	_make_scrollbar_visible_flag = true


func _on_Results_vertical_scroll_ended() -> void:
	""" When the scrollbar reaches the end, load more results """
	if _results_scroll_container.is_v_scrollbar_at_end():
		_add_results(_last_claim_search_args)


func _on_ResultsScrollContainer_v_scrollbar_visibility_changed() -> void:
	_adjust_results_margin_flag = true


func _on_SearchOptionsButton_pressed() -> void:
	_search_options_container.set_visible(
			not _search_options_container.is_visible())


func _on_SearchBar_text_entered(new_text: String) -> void:
	if _is_ready:
		_apply_search(new_text)


func _on_signal_buffer_timer_timeout() -> void:
	""" Execute the callbacks of the buffered signals """
	if _make_scrollbar_visible_flag:
		_make_scrollbar_visible_flag = false
		_add_results_until_scrollbar_visible()
	
	if _adjust_results_margin_flag:
		_adjust_results_margin_flag = false
		_make_scroll_bar_hug_window_edge()
		_apply_margin_if_scrollbar_visible()
