class_name ChannelWindow
extends MarginContainer

signal initialized

const BANNER_TARGET_ASPECT_RATIO: float = 6.25

export(PackedScene) var MediaListItem_scene: PackedScene = load("res://scenes/custom_nodes/media_list_item/media_list_item.tscn")
export(PackedScene) var ChannelCard_scene: PackedScene = load("res://scenes/custom_nodes/channel_card/channel_card.tscn")
export(String, FILE) var default_thumbnail_path: String = "res://icon.png"

# URL of the channel on the LBRY blockchain
var _channel_url: String = ""
var _channel_permanent_url: String = ""
# All the resolved data for this `_channel_url`
var _data: Dictionary
var _banner_img: Image
var _last_page_loaded: int = 0
# Right margin offset that should be applied so that the scroll bar hugs the
# edge of the window
var _scroll_container_offset: int = 0
var _initialized: bool = false
var _async_helper := AsyncHelper.new(self)
onready var _default_thumbnail: Image = load(default_thumbnail_path).get_data()
onready var _channel_card: ChannelCard = find_node("ChannelCard")
onready var _description_node: RichTextLabel = find_node("Description")
onready var _banner_node: TextureRect = find_node("Banner")
onready var _description_button: ButtonContainer = \
		find_node("DescriptionButton")
onready var _follow_button: Button = find_node("FollowButton")
onready var _media_node: DynamicGridContainer = find_node("Media")
onready var _scroll_container: ScrollContainer = find_node("ScrollContainer")


func _ready() -> void:
	if not _initialized:
		yield(self, "initialized")
	
	# Wait for this to completed before connecting `_on_FollowButton_toggled`
	# to prevent it from being called unnecessarily
	yield(_set_initial_FollowButton_state(), "completed")
	
	connect("resized", self, "_on_resized")
	_scroll_container.connect(
			"vertical_scroll_ended", self, "_on_vertical_scroll_ended")
	_scroll_container.get_v_scrollbar().connect(
			"visibility_changed", self, "_on_v_scrollbar_visibility_changed")
	_description_button.connect(
			"pressed", self, "_toggle_description_visibility")
	_follow_button.connect("toggled", self, "_on_FollowButton_toggled")
	
	_media_node.set_item_size(MediaListItem.SIZE)
	
	if len(_data) > 0:
		yield(_setup(), "completed")
		yield(_add_media_until_scrollbar_visible(), "completed")
		_make_scroll_bar_hug_window_edge()


func init(channel_url: String) -> void:
	"""
	Args:
		channel_url: The short or permanent LBRY URL of the channel
	"""
	var tracker: Dictionary
	if _initialized:
		tracker = _async_helper.connect_wrapped(
			get_tree(), "idle_frame", false)
		yield(_async_helper.wait_until_finished([tracker]), "completed")
		return
	
	tracker = _async_helper.connect_wrapped(
			SDK.resolve([channel_url]), "completed")
	yield(_async_helper.wait_until_finished([tracker]), "completed")
	var resolve_response: Dictionary = tracker["result"]
	if resolve_response.has("result"):
		_data = resolve_response["result"] \
				.get(resolve_response["result"].keys()[0])
		_channel_url = _data.get("short_url", _channel_url)
		_channel_permanent_url = _data.get("permanent_url", "")
	else:
		push_warning("`channel_url` did not resolve to anything")
	
	_initialized = true
	emit_signal("initialized")


func set_banner(banner_img: Image) -> void:
	var banner := ImageTexture.new()
	var cropped_banner_img: Image = _crop_to_aspect_ratio(banner_img,
			BANNER_TARGET_ASPECT_RATIO)
	banner.create_from_image(cropped_banner_img)
	_banner_node.set_texture(banner)
	
	_update_banner_node_min_size()


func set_channel_description(description: String) -> void:
	_description_node.set_text(description)


func _setup() -> void:
	var description: String
	
	description = _data.get("value", {}).get("description", "")
	
	var banner_url: String = _data \
			.get("value", {}) \
			.get("cover", {}) \
			.get("url", "")
	var tracker: Dictionary = _async_helper.connect_wrapped(
			Requests.get_image(banner_url), "completed")
	yield(_async_helper.wait_until_finished([tracker]), "completed")
	_banner_img = tracker["result"]
	if _banner_img.is_empty():
		_banner_img = _default_thumbnail
	
	var channel_card: ChannelCard = ChannelCard_scene.instance()
	channel_card.init(_channel_url)
	
	set_channel_description(description)
	set_banner(_banner_img)
	_channel_card.get_parent().add_child_below_node(
			_channel_card, channel_card)
	_channel_card.queue_free()
	_channel_card = channel_card


func _get_channel_media() -> Array:
	"""
	Finds media that is related to the current one by words in the title
	
	Must be called after `_setup()` has completed
	"""
	var media: Array = []
	var args: Dictionary = {
		"channel": _channel_url,
		"page": _last_page_loaded + 1,
		"order_by": "release_time",
		"has_channel_signature": true,
		"valid_channel_signature": true,
		"no_totals": true,
	}
	var permanent_search_options: Dictionary = Data.get_search_options()
	Helpers.dictionary_merge_into(args, permanent_search_options, true, false)
	var tracker: Dictionary = _async_helper.connect_wrapped(
			SDK.claim_search(args), "completed")
	yield(_async_helper.wait_until_finished([tracker]), "completed")
	var search_response: Dictionary = tracker["result"]
	_last_page_loaded += 1
	media = search_response.get("result", {}).get("items", [])
	return media


func _show_channel_media(media: Array) -> void:
	for item in media:
		# TODO: add UI option to filter out reposts
		var is_repost: bool = item.has("reposted_claim")
		var item_node: MediaListItem = MediaListItem_scene.instance()
		var channel_url: String
		if not is_repost:
			channel_url = item \
					.get("signing_channel", {}) \
					.get("short_url", "")
		else:
			channel_url = item \
					.get("reposted_claim", {}) \
					.get("signing_channel", {}) \
					.get("short_url", "")
		item_node.init(item)
		item_node.connect_media_button(
				"pressed", self, "_open_media_window", [item])
		item_node.connect_channel_button(
				"pressed", self, "_open_channel_window", [channel_url])
		item_node.set_channel_button_visible(false)
		_media_node.add_child(item_node)


func _add_media_until_scrollbar_visible() -> 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)
	"""
	var tracker: Dictionary
	while not _scroll_container.get_v_scrollbar().is_visible():
		_show_channel_media(yield(_get_channel_media(), "completed"))
		if not is_inside_tree():
			yield(self, "tree_entered")
		# `yield` to wait for the node to finish resizing
		tracker = _async_helper.connect_wrapped(
				get_tree(), "idle_frame", false)
		yield(_async_helper.wait_until_finished([tracker]), "completed")


func _toggle_description_visibility() -> void:
	_description_node.set_visible(not _description_node.is_visible())


func _set_initial_FollowButton_state() -> void:
	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"]
	if (
			(_channel_url != ""
				and _channel_url in followed_channels)
			or (_channel_permanent_url != ""
				and _channel_permanent_url in followed_channels)
	):
		_follow_button.set_pressed(true)
		_follow_button.set_text("Unfollow")
	else:
		_follow_button.set_pressed(false)
		_follow_button.set_text("Follow")


func _crop_to_aspect_ratio(img: Image, target_aspect: float) -> Image:
	"""
	Performs the most minimal crop to the center to make `img` have the aspect
	ratio `target_aspect`
	"""
	var cropped_img := Image.new()
	var img_aspect: float = img.get_size().aspect()
	var img_width: int = img.get_width()
	var img_height: int = img.get_height()
	
	if img_aspect > target_aspect:
		var crop_x_by := int(
				img_aspect * img_height
				- target_aspect * img_height
		)
		cropped_img.create(img_width - crop_x_by, img_height, true,
				img.get_format())
		cropped_img.blit_rect(img,
				Rect2(crop_x_by / 2.0, 0, img_width - crop_x_by, img_height),
				Vector2.ZERO)
	elif img_aspect < target_aspect:
		var crop_y_by := int(
				(1 / img_aspect) * img_width
				- (1 / target_aspect) * img_width
		)
		cropped_img.create(img_width, img_height - crop_y_by, true,
				img.get_format())
		cropped_img.blit_rect(img,
				Rect2(0, crop_y_by / 2.0, img_width, img_height - crop_y_by),
				Vector2.ZERO)
	
	cropped_img.generate_mipmaps()
	return cropped_img


func _update_banner_node_min_size() -> void:
	"""
	Must set the `rect_min_size` since when using `expand` on a TextureRect
	the size of the image depends on the size of the node
	"""
	var banner_height: float = \
			_banner_node.get_size().x / BANNER_TARGET_ASPECT_RATIO
	_banner_node.set_custom_minimum_size(
			Vector2(_banner_node.get_custom_minimum_size().x, banner_height))


func _make_scroll_bar_hug_window_edge() -> void:
	var scroll_container_right_edge: float = (
			_scroll_container.get_global_position().x
			+ _scroll_container.get_size().x
	)
	var diff := int(OS.get_window_size().x - scroll_container_right_edge)
	_scroll_container_offset = -diff
	if _scroll_container.get_v_scrollbar().is_visible_in_tree():
		add_constant_override("margin_right", _scroll_container_offset)


func _open_media_window(data: Dictionary) -> void:
	# Can't specify the exact node type here because it causes a cyclic
	# reference error
	var media_window: Control = \
			get_parent().get_window_type("MediaWindow").instance()
	media_window.init(data)
	get_parent().set_window(media_window)


func _open_channel_window(channel_url: String) -> void:
	"""
	Args:
		channel_url: URL of the channel to open. Not necessarily the same as
			the current channel due to reposts
	"""
	if channel_url == _channel_url:
		return
	# Can't specify the exact node type here because it causes a cyclic
	# reference error
	var channel_window: Control = \
			get_parent().get_window_type("ChannelWindow").instance()
	channel_window.init(channel_url)
	get_parent().set_window(channel_window)


func _on_resized() -> void:
	# `yield` to wait for the node to finish resizing
	var tracker: Dictionary = _async_helper.connect_wrapped(
			get_tree(), "idle_frame", false)
	yield(_async_helper.wait_until_finished([tracker]), "completed")
	_update_banner_node_min_size()
	_add_media_until_scrollbar_visible()


func _on_vertical_scroll_ended() -> void:
	if _scroll_container.is_v_scrollbar_at_end():
		_show_channel_media(yield(_get_channel_media(), "completed"))


func _on_v_scrollbar_visibility_changed() -> void:
	""" Ensure the margin is maintained when the scroll bar is not visible """
	var right_margin: int = (
			_scroll_container_offset
			if _scroll_container.get_v_scrollbar().is_visible_in_tree()
			else 0
	)
	add_constant_override("margin_right", right_margin)


func _on_FollowButton_toggled(button_pressed: bool) -> void:
	if button_pressed:
		LBRYActions.follow_channel(_channel_url)
		_follow_button.set_text("Unfollow")
	else:
		LBRYActions.unfollow_channel(_channel_url)
		_follow_button.set_text("Follow")
