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

# Untested yet
## A single command like !lurk
class_name TwitchCommand

## Constant to convert from seconds to milliseconds
const S_TO_MS = 1000

static var ALL_COMMANDS: Array[TwitchCommand] = []

## Called when the command got received in the right format
signal command_received(from_username: String, info: TwitchCommandInfo, args: PackedStringArray)

## Called when the command got received in the wrong format
signal received_invalid_command(from_username: String, info: TwitchCommandInfo, args: PackedStringArray)

## Called when the user tries to use the command that is still on cooldown (remaining cooldown in seconds)
signal cooldown(from_username: String, info: TwitchCommandInfo, args: PackedStringArray, cooldown_remaining_in_s: float)

## Required permission to execute the command
enum PermissionFlag {
	EVERYONE = 0,
	VIP = 1,
	SUB = 2,
	MOD = 4,
	STREAMER = 8,
	MOD_STREAMER = 12, # Mods and the streamer
	NON_REGULAR = 15 # Everyone but regular viewers
}

## Where the command should be accepted
enum WhereFlag {
	CHAT = 1,
	WHISPER = 2,
	ANYWHERE = 3
}

@export var command_prefixes : Array[String] = ["!"]
## Name Command
@export var command: String
## Optional names of commands
@export var aliases: Array[String]
## Description for the user
@export_multiline var description: String

## Minimal amount of argument 0 means no argument needed
@export var args_min: int = 0
## Max amount of arguments -1 means infinite
@export var args_max: int = -1
## Wich role of user is allowed to use it
@export var permission_level: PermissionFlag = PermissionFlag.EVERYONE
## Where is it allowed to use chat or whisper or both
@export var where: WhereFlag = WhereFlag.CHAT
## All allowed users empty array means everyone
@export var allowed_users: Array[String] = []
## All chatrooms where the command listens to
@export var listen_to_chatrooms: Array[String] = []
## Determines if the aliases and commands should be case sensitive or not
@export var case_insensitive: bool = true
## Cooldown per user
@export var user_cooldown: float = 0
## Global cooldown for the command
@export var global_cooldown: float = 0

## The eventsub to listen for chatmessages
@export var eventsub: TwitchEventsub

## Cooldowns per user Key: Username | Value: Time until it can be used again
var _user_cooldowns: Dictionary[String, float] = {}
## Time until it can be used again
var _global_cooldown: float

static func create(
		eventsub: TwitchEventsub,
		cmd_name: String,
		callable: Callable,
		min_args: int = 0,
		max_args: int = 0,
		permission_level: int = PermissionFlag.EVERYONE,
		where: int = WhereFlag.CHAT,
		allowed_users: Array[String] = [],
		listen_to_chatrooms: Array[String] = [],
		case_insensitive: bool = true) -> TwitchCommand:
	var command := TwitchCommand.new()
	command.eventsub = eventsub
	command.command = cmd_name
	command.command_received.connect(callable)
	command.args_min = min_args
	command.args_max = max_args
	command.permission_level = permission_level
	command.where = where
	command.allowed_users = allowed_users
	command.listen_to_chatrooms = listen_to_chatrooms
	command.case_insensitive = case_insensitive
	return command


func _enter_tree() -> void:
	if eventsub == null: eventsub = TwitchEventsub.instance
	if eventsub != null:
		eventsub.event.connect(_on_event)
	ALL_COMMANDS.append(self)

	for prefix: String in command_prefixes:
		if prefix.length() > 1:
			push_error("TwitchCommand supports only 1 character for command prefix. '%s' is used by %s" % [prefix, get_path()])


func _exit_tree() -> void:
	if eventsub != null:
		eventsub.event.disconnect(_on_event)
	ALL_COMMANDS.erase(self)


func _on_event(type: StringName, data: Dictionary) -> void:
	if type == TwitchEventsubDefinition.CHANNEL_CHAT_MESSAGE.value:
		if where & WhereFlag.CHAT != WhereFlag.CHAT: return
		var message : String = data.message.text
		var username : String = data.chatter_user_login
		var channel_name : String = data.broadcaster_user_login
		if not _should_handle(message, username, channel_name): return
		var chat_message = TwitchChatMessage.from_json(data)
		_handle_command(username, message, channel_name, chat_message)

	if type == TwitchEventsubDefinition.USER_WHISPER_MESSAGE.value:
		if where & WhereFlag.WHISPER != WhereFlag.WHISPER: return
		var message : String = data.whisper.text
		var from_user : String = data.from_user_login
		if not _should_handle(message, from_user, from_user): return
		_handle_command(from_user, message, data.to_user_login, data)


func add_alias(alias: String) -> void:
	aliases.append(alias)


func _should_handle(message: String, username: String, channel_name: String) -> bool:
	if not listen_to_chatrooms.is_empty() && not listen_to_chatrooms.has(channel_name): return false
	if not allowed_users.is_empty() && not allowed_users.has(username): return false
	if not command_prefixes.has(message.left(1)): return false

	# remove the command symbol in front
	message = message.right(-1)
	var split: PackedStringArray = message.split(" ", true, 1)
	var current_command: String = split[0]
	var alias_compare_function: Callable = func(cmd) -> bool:
		if case_insensitive:
			return current_command.nocasecmp_to(cmd) == 0
		else:
			return current_command.casecmp_to(cmd) == 0

	var is_alias: bool = aliases.any(alias_compare_function)
	var is_command: bool = alias_compare_function.call(command)

	return is_command || is_alias



func _handle_command(from_username: String, raw_message: String, to_user: String, data: Variant) -> void:
	# remove the command symbol in front
	raw_message = raw_message.right(-1)
	var cmd_msg = raw_message.split(" ", true, 1)
	var message = ""
	var arg_array : PackedStringArray = []
	var command = cmd_msg[0]
	var info = TwitchCommandInfo.new(self, to_user, from_username, arg_array, data)

	# Handle Permission check
	var premission_required = permission_level != 0
	if premission_required:
		var user_perm_flags = _get_perm_flag_from_tags(data)
		if user_perm_flags & permission_level == 0:
			received_invalid_command.emit(from_username, info, arg_array)
			return

	# Handle Argument parsing
	if cmd_msg.size() > 1:
		message = cmd_msg[1]
		arg_array.append_array(message.split(" ", false))
		var to_less_arguments = arg_array.size() < args_min
		var to_much_arguments = arg_array.size() > args_max
		if to_much_arguments && args_max != -1 || to_less_arguments:
			received_invalid_command.emit(from_username, info, arg_array)
			return
	if arg_array.size() == 0 && args_min > 0:
		received_invalid_command.emit(from_username, info, arg_array)
		return

	# Handle Cooldowns
	if user_cooldown > 0:
		var current_user_cooldown: float = _user_cooldowns.get_or_add(from_username, 0)
		if current_user_cooldown >= Time.get_ticks_msec():
			var cooldown_left: float = (current_user_cooldown - Time.get_ticks_msec()) / 1000
			cooldown.emit(from_username, info, arg_array, cooldown_left)
			return
		_user_cooldowns.set(from_username, Time.get_ticks_msec() + (user_cooldown * S_TO_MS))

	if global_cooldown > 0:
		if _global_cooldown >= Time.get_ticks_msec():
			var cooldown_left: float = (_global_cooldown - Time.get_ticks_msec()) / 1000
			cooldown.emit(from_username, info, arg_array, cooldown_left)
			return
		_global_cooldown = Time.get_ticks_msec() + (global_cooldown * S_TO_MS)

	# Executing the command
	if arg_array.size() == 0:
		var empty_args: Array[String] = []
		if args_max > 0:
			command_received.emit(from_username, info, empty_args)
		else:
			command_received.emit(from_username, info, empty_args)
	else:
		command_received.emit(from_username, info, arg_array)


func _get_perm_flag_from_tags(data : Variant) -> int:
	var flag: int = 0
	if data is TwitchChatMessage:
		var message: TwitchChatMessage = data as TwitchChatMessage
		for badge in message.badges:
			match badge.set_id:
				"broadcaster": flag += PermissionFlag.STREAMER
				"vip": flag += PermissionFlag.VIP
				"moderator": flag += PermissionFlag.MOD
				"subscriber": flag += PermissionFlag.SUB
	return flag
