@icon("res://addons/twitcher/assets/service-icon.svg")
@tool
extends Twitcher

## Access to the Twitch API. Combines all the stuff the library provides.
## Makes some actions easier to use.
class_name TwitchService

const TwitchEditorSettings = preload("res://addons/twitcher/editor/twitch_editor_settings.gd")
## When the poll doesn't end after the offical endtime + POLL_TIMEOUT_MS. The wait loop for poll end
## event will be stopped to prevent endless loops.
const POLL_TIMEOUT_MS: int = 30000

static var _log: TwitchLogger = TwitchLogger.new("TwitchService")

static var instance: TwitchService

@export var oauth_setting: OAuthSetting:
	set(val):
		if oauth_setting != null:
			oauth_setting.changed.disconnect(update_configuration_warnings)
		oauth_setting = val
		if val != null:
			oauth_setting.changed.connect(update_configuration_warnings)
			_set_in_child("oauth_setting", val)
		update_configuration_warnings()
@export var scopes: OAuthScopes:
	set(val):
		scopes = val
		if val != null:
			_set_in_child("scopes", val)
			update_configuration_warnings()
@export var token: OAuthToken:
	set(val):
		token = val
		if val != null:
			_set_in_child("token", val)
			update_configuration_warnings()

@onready var auth: TwitchAuth
@onready var eventsub: TwitchEventsub
@onready var api: TwitchAPI
@onready var irc: TwitchIRC
@onready var media_loader: TwitchMediaLoader

var _user_cache: Dictionary[String, TwitchUser] = {}

## Cache for the current user so that no roundtrip has to be done every time get_current_user will be called
var _current_user: TwitchUser

var _commands: Dictionary[String, TwitchCommand] = {}

func _init() -> void:
	child_entered_tree.connect(_on_child_entered)
	child_exiting_tree.connect(_on_child_exiting)


func _ready() -> void:
	_log.d("is ready")
	if not is_instance_valid(token): token = TwitchEditorSettings.game_oauth_token
	if not is_instance_valid(oauth_setting): oauth_setting = TwitchEditorSettings.game_oauth_setting


func _enter_tree() -> void:
	if instance == null: instance = self


func _exit_tree() -> void:
	if instance == self: instance = null


func _on_child_entered(node: Node) -> void:
	if node is TwitchAuth: auth = node
	if node is TwitchAPI: api = node
	if node is TwitchEventsub: eventsub = node
	if node is TwitchIRC: irc = node
	if node is TwitchMediaLoader: media_loader = node

	if "token" in node && token != null:
		node.token = token
	if "scopes" in node && scopes != null:
		node.scopes = scopes
	if "oauth_setting" in node && oauth_setting != null:
		node.oauth_setting = oauth_setting
	if node.has_signal(&"unauthenticated"):
		node.unauthenticated.connect(_on_unauthenticated)
	update_configuration_warnings()


func _set_in_child(property: String, value: Variant) -> void:
	for child in get_children():
		if property in child: child[property] = value


func _on_child_exiting(node: Node) -> void:
	if node is TwitchAuth: auth = null
	if node is TwitchAPI: api = null
	if node is TwitchEventsub: eventsub = null
	if node is TwitchIRC: irc = null
	if node is TwitchMediaLoader: media_loader = null

	if node.has_signal(&"unauthenticated"):
		node.unauthenticated.disconnect(_on_unauthenticated)
	update_configuration_warnings()


## Call this to setup the complete Twitch integration whenever you need.
## It boots everything up this Lib supports.
func setup() -> bool:
	if is_instance_valid(auth):
		if not await auth.authorize(): return false
	elif not token.is_token_valid():
		push_error("Authorization Node got removed, can't setup twitch service")
		return false
	await propagate_call(&"do_setup")
	for child in get_children():
		if child.has_method(&"wait_setup"):
			await child.wait_setup()

	_log.i("TwitchService setup")
	return true


## Removes the token so that
func unsetup() -> void:
	await propagate_call(&"do_unsetup")
	for child in get_children():
		if child.has_method(&"wait_unsetup"):
			await child.wait_unsetup()


## Checks if the correctly setup
func is_configured() -> bool:
	return _get_configuration_warnings().is_empty()


func _get_configuration_warnings() -> PackedStringArray:
	var result: PackedStringArray = []
	if oauth_setting == null:
		result.append("OAuthSetting Resource is missing")
	else:
		var oauth_setting_problems : PackedStringArray = oauth_setting.get_valididation_problems()
		if not oauth_setting_problems.is_empty():
			result.append("OAuthSetting Resource is invalid")
			result.append_array(oauth_setting_problems)
	if scopes == null:
		result.append("OAuthScopes Resource is missing")
	if token == null:
		result.append("OAuthToken Resource is missing")
	return result


func _on_unauthenticated() -> void:
	auth.authorize()

#
# Convinient Proxy Methods
#
#region User


## Get data about a user by USER_ID see get_user for by username
func get_user_by_id(user_id: String) -> TwitchUser:
	if _user_cache.has(user_id): return _user_cache[user_id]
	if api == null:
		_log.e("Please setup a TwitchAPI Node into TwitchService.")
		return null
	if user_id == null || user_id == "": return null
	var opt = TwitchGetUsers.Opt.new()
	opt.id = [user_id] as Array[String]
	var user_data : TwitchGetUsers.Response = await api.get_users(opt)
	if user_data.data.is_empty(): return null
	var user: TwitchUser = user_data.data[0]
	_user_cache[user_id] = user
	return user


## Get data about a user by USERNAME see get_user_by_id for by user_id
func get_user(username: String) -> TwitchUser:
	username = username.trim_prefix("@")
	if _user_cache.has(username): return _user_cache[username]
	if api == null:
		_log.e("Please setup a TwitchAPI Node into TwitchService.")
		return null
	var opt = TwitchGetUsers.Opt.new()
	opt.login = [username] as Array[String]
	var user_data : TwitchGetUsers.Response = await api.get_users(opt)
	if user_data.data.is_empty():
		_log.e("Username was not found: %s" % username)
		return null
	var user: TwitchUser = user_data.data[0]
	_user_cache[username] = user
	return user



## Get data about a currently authenticated user (caches the value)
func get_current_user() -> TwitchUser:
	if _current_user != null:
		return _current_user

	if api == null:
		_log.e("Please setup a TwitchAPI Node into TwitchService.")
		return null

	var user_data : TwitchGetUsers.Response = await api.get_users(null)
	_current_user = user_data.data[0]
	return _current_user

## Get the image of an user
func load_profile_image(user: TwitchUser) -> ImageTexture:
	return await media_loader.load_profile_image(user)

#endregion
#region EventSub


## Refer to https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types/ for details on
## which API versions are available and which conditions are required.
func subscribe_event(definition: TwitchEventsubDefinition, conditions: Dictionary) -> TwitchEventsubConfig:
	if definition == null:
		_log.e("TwitchEventsubDefinition is null")
		return

	var config = TwitchEventsubConfig.create(definition, conditions)
	await eventsub.subscribe(config)
	return config


## Waits for connection to eventsub. Eventsub is ready to subscribe events.
func wait_for_eventsub_connection() -> void:
	if eventsub == null:
		_log.e("TwitchEventsub Node is missing")
		return
	await eventsub.wait_for_connection()


## Returns all of the eventsub subscriptions (variable is a copy so you can freely modify it)
func get_subscriptions() -> Array[TwitchEventsubConfig]:
	if eventsub == null:
		_log.e("TwitchEventsub Node is missing")
		return []
	return eventsub.get_subscriptions()

#endregion

#region Chat

func chat(message: String, broadcaster: TwitchUser = null, sender: TwitchUser = null) -> void:
	var current_user = await get_current_user()
	if not sender:
		if not current_user: return
		sender = current_user
	if not broadcaster:
		if not current_user: return
		broadcaster = current_user
	var body = TwitchSendChatMessage.Body.create(broadcaster.id, sender.id, message)
	api.send_chat_message(body)


## Sends out a shoutout to a specific user
func shoutout(user: TwitchUser, broadcaster: TwitchUser = null, moderator: TwitchUser = null) -> void:
	var current_user: TwitchUser = await get_current_user()

	if not broadcaster:
		if not current_user: return
		broadcaster = current_user

	if not moderator:
		if not current_user: return
		moderator = current_user
	api.send_a_shoutout(broadcaster.id, moderator.id, user.id)


## Sends a announcement message to the chat
func announcment(message: String, color: TwitchAnnouncementColor = TwitchAnnouncementColor.PRIMARY, broadcaster: TwitchUser = null, moderator: TwitchUser = null):
	var current_user: TwitchUser = await get_current_user()
	if not broadcaster:
		if not current_user: return
		broadcaster = current_user

	if not moderator:
		if not current_user: return
		moderator = current_user

	var body = TwitchSendChatAnnouncement.Body.new()
	body.message = message
	body.color = color.value
	api.send_chat_announcement(body, moderator.id, broadcaster.id)


## Add a new command handler and register it for a command.
## The callback will receive [code]from_username: String, info: TwitchCommandInfo, args: PackedStringArray[/code][br]
## Args are optional depending on the configuration.[br]
## args_max == -1 => no upper limit for arguments
func add_command(command: String, callback: Callable, args_min: int = 0, args_max: int = -1,
	permission_level : TwitchCommand.PermissionFlag = TwitchCommand.PermissionFlag.EVERYONE,
	where : TwitchCommand.WhereFlag = TwitchCommand.WhereFlag.CHAT, user_cooldown: float = 0,
	global_cooldown: float = 0) -> TwitchCommand:
	var command_node = TwitchCommand.new()
	command_node.command = command
	command_node.command_received.connect(callback)
	command_node.args_min = args_min
	command_node.args_max = args_max
	command_node.permission_level = permission_level
	command_node.where = where
	command_node.user_cooldown = user_cooldown
	command_node.global_cooldown = global_cooldown
	add_child(command_node)
	_log.i("Register command %s" % command)
	return command_node


## Removes a command
func remove_command(command: String) -> void:
	_log.i("Remove command %s" % command)
	var command_node: TwitchCommand = _commands.get(command, null)
	if command_node != null:
		command_node.queue_free()
		_commands.erase(command)


## Whispers to another user.
## @deprecated not supported by twitch anymore
func whisper(message: String, username: String) -> void:
	_log.e("Whipser from bots aren't supported by Twitch anymore. See https://dev.twitch.tv/docs/irc/chat-commands/ at /w")


## Returns the definition of emotes for given channel or for the global emotes.
## Key: EmoteID as String | Value: TwitchGlobalEmote / TwitchChannelEmote
func get_emotes_data(channel_id: String = "global") -> Dictionary:
	return await media_loader.get_cached_emotes(channel_id)


## Returns the definition of badges for given channel or for the global bages.
## Key: category / versions / badge_id | Value: TwitchChatBadge
func get_badges_data(channel_id: String = "global") -> Dictionary[String, TwitchChatBadge]:
	return await media_loader.get_cached_badges(channel_id)


## Gets the requested emotes.
## Key: EmoteID as String | Value: SpriteFrame
func get_emotes(ids: Array[String]) -> Dictionary[String, SpriteFrames]:
	return await media_loader.get_emotes(ids)


## Gets the requested emotes in the specified theme, scale and type.
## Loads from cache if possible otherwise downloads and transforms them.
## Key: TwitchEmoteDefinition | Value SpriteFrames
func get_emotes_by_definition(emotes: Array[TwitchEmoteDefinition]) -> Dictionary[TwitchEmoteDefinition, SpriteFrames]:
	return await media_loader.get_emotes_by_definition(emotes)


func poll(title: String, choices: Array[String], duration: int = 60, channel_points_voting_enabled: bool = false, channel_points_per_vote: int = 1000, broadcaster_id: String = "") -> Dictionary:
	if broadcaster_id == "": broadcaster_id = _current_user.id
	var body_choices: Array[TwitchCreatePoll.BodyChoices] = []
	for choice: String in choices:
		var body_choice = TwitchCreatePoll.BodyChoices.create(choice)
		body_choices.append(body_choice)
	duration = clamp(duration, 15, 1800)
	var poll_body: TwitchCreatePoll.Body = TwitchCreatePoll.Body.create(broadcaster_id, title, body_choices, duration)
	if channel_points_voting_enabled:
		poll_body.channel_points_per_vote = channel_points_per_vote
		poll_body.channel_points_voting_enabled = channel_points_voting_enabled
	var poll_response: TwitchCreatePoll.Response = await api.create_poll(poll_body)
	if poll_response.response.response_code != 200:
		var error_message: String = poll_response.response.response_data.get_string_from_utf8()
		push_error("Can't create poll response cause of ", error_message)
		return {}
	var poll: TwitchPoll = poll_response.data[0]
	var poll_end_time: int = Time.get_ticks_msec() + duration * 1000 + POLL_TIMEOUT_MS
	var event: TwitchEventsub.Event
	if eventsub && eventsub.has_subscription(TwitchEventsubDefinition.CHANNEL_POLL_END, {&"broadcaster_user_id": broadcaster_id}):
		var poll_ended: bool
		while not poll_ended:
			if poll_end_time < Time.get_ticks_msec():
				return {}
			event = await eventsub.event_received
			if event.type != TwitchEventsubDefinition.CHANNEL_POLL_END: continue
			if event.data[&"id"] != poll.id: continue
			break
	else:
		_log.i("Can't wait for poll end. Either eventsub is not set ot it is not listenting to ending polls")
		return {}
	return event.data

#endregion
#region Cheermotes

## Returns the data of the Cheermotes.
func get_cheermote_data() -> Array[TwitchCheermote]:
	if media_loader == null:
		_log.e("TwitchMediaLoader was not set within %s" % get_tree_string())
		return []
	await media_loader.preload_cheemote()
	return media_loader.all_cheermotes()


## Returns all cheertiers in form of:
## Key: TwitchCheermote.Tiers | Value: SpriteFrames
func get_cheermotes(definition: TwitchCheermoteDefinition) -> Dictionary:
	return await media_loader.get_cheermotes(definition)

#endregion
