from common.log import logUtils as log
from common.ripple import userUtils
from constants import exceptions
from constants import messageTemplates
from constants import serverPackets
from events import logoutEvent
from objects import fokabot
from objects import glob


def joinChannel(userID = 0, channel = "", token = None, toIRC = True, force=False):
	"""
	Join a channel

	:param userID: user ID of the user that joins the channel. Optional. token can be used instead.
	:param token: user token object of user that joins the channel. Optional. userID can be used instead.
	:param channel: channel name
	:param toIRC: if True, send this channel join event to IRC. Must be true if joining from bancho. Default: True
	:param force: whether to allow game clients to join #spect_ and #multi_ channels
	:return: 0 if joined or other IRC code in case of error. Needed only on IRC-side
	"""
	try:
		# Get token if not defined
		if token is None:
			token = glob.tokens.getTokenFromUserID(userID)
			# Make sure the token exists
			if token is None:
				raise exceptions.userNotFoundException
		else:
			token = token

		# Normal channel, do check stuff
		# Make sure the channel exists
		if channel not in glob.channels.channels:
			raise exceptions.channelUnknownException()

		# Make sure a game client is not trying to join a #multi_ or #spect_ channel manually
		channelObject = glob.channels.channels[channel]
		if channelObject.isSpecial and not token.irc and not force:
			raise exceptions.channelUnknownException()

		# Add the channel to our joined channel
		token.joinChannel(channelObject)

		# Send channel joined (IRC)
		if glob.irc and not toIRC:
			glob.ircServer.banchoJoinChannel(token.username, channel)

		# Console output
		log.info("{} joined channel {}".format(token.username, channel))

		# IRC code return
		return 0
	except exceptions.channelNoPermissionsException:
		log.warning("{} attempted to join channel {}, but they have no read permissions".format(token.username, channel))
		return 403
	except exceptions.channelUnknownException:
		log.warning("{} attempted to join an unknown channel ({})".format(token.username, channel))
		return 403
	except exceptions.userAlreadyInChannelException:
		log.warning("User {} already in channel {}".format(token.username, channel))
		return 403
	except exceptions.userNotFoundException:
		log.warning("User not connected to IRC/Bancho")
		return 403	# idk

def partChannel(userID = 0, channel = "", token = None, toIRC = True, kick = False, force=False):
	"""
	Part a channel

	:param userID: user ID of the user that parts the channel. Optional. token can be used instead.
	:param token: user token object of user that parts the channel. Optional. userID can be used instead.
	:param channel: channel name
	:param toIRC: if True, send this channel join event to IRC. Must be true if joining from bancho. Optional. Default: True
	:param kick: if True, channel tab will be closed on client. Used when leaving lobby. Optional. Default: False
	:param force: whether to allow game clients to part #spect_ and #multi_ channels
	:return: 0 if joined or other IRC code in case of error. Needed only on IRC-side
	"""
	try:
		# Make sure the client is not drunk and sends partChannel when closing a PM tab
		if not channel.startswith("#"):
			return

		# Get token if not defined
		if token is None:
			token = glob.tokens.getTokenFromUserID(userID)
			# Make sure the token exists
			if token is None:
				raise exceptions.userNotFoundException()
		else:
			token = token

		# Determine internal/client name if needed
		# (toclient is used clientwise for #multiplayer and #spectator channels)
		channelClient = channel
		if channel == "#spectator":
			if token.spectating is None:
				s = userID
			else:
				s = token.spectatingUserID
			channel = "#spect_{}".format(s)
		elif channel == "#multiplayer":
			channel = "#multi_{}".format(token.matchID)
		elif channel.startswith("#spect_"):
			channelClient = "#spectator"
		elif channel.startswith("#multi_"):
			channelClient = "#multiplayer"

		# Make sure the channel exists
		if channel not in glob.channels.channels:
			raise exceptions.channelUnknownException()

		# Make sure a game client is not trying to join a #multi_ or #spect_ channel manually
		channelObject = glob.channels.channels[channel]
		if channelObject.isSpecial and not token.irc and not force:
			raise exceptions.channelUnknownException()

		# Make sure the user is in the channel
		if channel not in token.joinedChannels:
			raise exceptions.userNotInChannelException()

		# Part channel (token-side and channel-side)
		token.partChannel(channelObject)

		# Delete temporary channel if everyone left
		if "chat/{}".format(channelObject.name) in glob.streams.streams:
			if channelObject.temp and len(glob.streams.streams["chat/{}".format(channelObject.name)].clients) - 1 == 0:
				glob.channels.removeChannel(channelObject.name)

		# Force close tab if needed
		# NOTE: Maybe always needed, will check later
		if kick:
			token.enqueue(serverPackets.channelKicked(channelClient))

		# IRC part
		if glob.irc and toIRC:
			glob.ircServer.banchoPartChannel(token.username, channel)

		# Console output
		log.info("{} parted channel {} ({})".format(token.username, channel, channelClient))

		# Return IRC code
		return 0
	except exceptions.channelUnknownException:
		log.warning("{} attempted to part an unknown channel ({})".format(token.username, channel))
		return 403
	except exceptions.userNotInChannelException:
		log.warning("{} attempted to part {}, but he's not in that channel".format(token.username, channel))
		return 442
	except exceptions.userNotFoundException:
		log.warning("User not connected to IRC/Bancho")
		return 442	# idk

def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
	"""
	Send a message to osu!bancho and IRC server

	:param fro: sender username. Optional. token can be used instead
	:param to: receiver channel (if starts with #) or username
	:param message: text of the message
	:param token: sender token object. Optional. fro can be used instead
	:param toIRC: if True, send the message to IRC. If False, send it to Bancho only. Default: True
	:return: 0 if joined or other IRC code in case of error. Needed only on IRC-side
	"""
	try:
		#tokenString = ""
		# Get token object if not passed
		if token is None:
			token = glob.tokens.getTokenFromUsername(fro)
			if token is None:
				raise exceptions.userNotFoundException()
		else:
			# token object alredy passed, get its string and its username (fro)
			fro = token.username
			#tokenString = token.token

		# Make sure this is not a tournament client
		# if token.tournament:
		# 	raise exceptions.userTournamentException()

		# Make sure the user is not in restricted mode
		if token.restricted:
			raise exceptions.userRestrictedException()

		# Make sure the user is not silenced
		if token.isSilenced():
			raise exceptions.userSilencedException()

		# Redirect !report to FokaBot
		if message.startswith("!report"):
			to = glob.BOT_NAME

		# Determine internal name if needed
		# (toclient is used clientwise for #multiplayer and #spectator channels)
		toClient = to
		if to == "#spectator":
			if token.spectating is None:
				s = token.userID
			else:
				s = token.spectatingUserID
			to = "#spect_{}".format(s)
		elif to == "#multiplayer":
			to = "#multi_{}".format(token.matchID)
		elif to.startswith("#spect_"):
			toClient = "#spectator"
		elif to.startswith("#multi_"):
			toClient = "#multiplayer"

		# Make sure the message is valid
		if not message.strip():
			raise exceptions.invalidArgumentsException()

		# Truncate message if > 2048 characters
		message = message[:2048]+"..." if len(message) > 2048 else message

		# Check for word filters
		message = glob.chatFilters.filterMessage(message)

		# Build packet bytes
		packet = serverPackets.sendMessage(token.username, toClient, message)

		# Send the message
		isChannel = to.startswith("#")
		if isChannel:
			# CHANNEL
			# Make sure the channel exists
			if to not in glob.channels.channels:
				raise exceptions.channelUnknownException()

			# Make sure the channel is not in moderated mode
			if glob.channels.channels[to].moderated and not token.admin:
				raise exceptions.channelModeratedException()

			# Make sure we are in the channel
			if to not in token.joinedChannels:
				# I'm too lazy to put and test the correct IRC error code here...
				# but IRC is not strict at all so who cares
				raise exceptions.channelNoPermissionsException()

			# Make sure we have write permissions
			if not glob.channels.channels[to].publicWrite and not token.admin:
				raise exceptions.channelNoPermissionsException()

			# Add message in buffer
			token.addMessageInBuffer(to, message)

			# Everything seems fine, build recipients list and send packet
			glob.streams.broadcast("chat/{}".format(to), packet, but=[token.token])
		else:
			# USER
			# Make sure recipient user is connected
			recipientToken = glob.tokens.getTokenFromUsername(to)
			if recipientToken is None:
				raise exceptions.userNotFoundException()

			# Make sure the recipient is not a tournament client
			#if recipientToken.tournament:
			#	raise exceptions.userTournamentException()

			# Make sure the recipient is not restricted or we are FokaBot
			if recipientToken.restricted and fro.lower() != glob.BOT_NAME:
				raise exceptions.userRestrictedException()

			# TODO: Make sure the recipient has not disabled PMs for non-friends or he's our friend

			# Away check
			if recipientToken.awayCheck(token.userID):
				sendMessage(to, fro, "\x01ACTION is away: {}\x01".format(recipientToken.awayMessage))

			# Check message templates (mods/admins only)
			if message in messageTemplates.templates and token.admin:
				sendMessage(fro, to, messageTemplates.templates[message])

			# Everything seems fine, send packet
			recipientToken.enqueue(packet)

		# Send the message to IRC
		if glob.irc and toIRC:
			messageSplitInLines = message.encode("latin-1").decode("utf-8").split("\n")
			for line in messageSplitInLines:
				if line == messageSplitInLines[:1] and line == "":
					continue
				glob.ircServer.banchoMessage(fro, to, line)

		# Spam protection (ignore FokaBot)
		if token.userID > 999:
			token.spamProtection()

		# Fokabot message
		if isChannel or to == glob.BOT_NAME:
			fokaMessage = fokabot.fokabotResponse(token.username, to, message)
			if fokaMessage:
				sendMessage(glob.BOT_NAME, to if isChannel else fro, fokaMessage)

		# File and discord logs (public chat only) (to make public only, if to.startswith("#") and not)
		if not (message.startswith("\x01ACTION is playing") and to.startswith("#spect_")):
			if isChannel:
				log.chat("[PUBLIC] {fro} @ {to}: {message}".format(fro=token.username, to=to, message=message.encode("latin-1").decode("utf-8")))
				glob.schiavo.sendChatlog("**{fro} @ {to}:** {message}".format(fro=token.username, to=to, message=message.encode("latin-1").decode("utf-8")))
			else:
				log.pm("[PRIVATE] {fro} @ {to}: {message}".format(fro=token.username, to=to, message=message.encode("latin-1").decode("utf-8")))
				glob.schiavo.sendChatlog("**{fro} @ {to}:** {message}".format(fro=token.username, to=to, message=message.encode("latin-1").decode("utf-8")))
		return 0
	except exceptions.userSilencedException:
		token.enqueue(serverPackets.silenceEndTime(token.getSilenceSecondsLeft()))
		log.warning("{} tried to send a message during silence".format(token.username))
		return 404
	except exceptions.channelModeratedException:
		log.warning("{} tried to send a message to a channel that is in moderated mode ({})".format(token.username, to))
		return 404
	except exceptions.channelUnknownException:
		log.warning("{} tried to send a message to an unknown channel ({})".format(token.username, to))
		return 403
	except exceptions.channelNoPermissionsException:
		log.warning("{} tried to send a message to channel {}, but they have no write permissions".format(token.username, to))
		return 404
	except exceptions.userRestrictedException:
		log.warning("{} tried to send a message {}, but the recipient is in restricted mode".format(token.username, to))
		return 404
	except exceptions.userTournamentException:
		log.warning("{} tried to send a message {}, but the recipient is a tournament client".format(token.username, to))
		return 404
	except exceptions.userNotFoundException:
		log.warning("User not connected to IRC/Bancho")
		return 401
	except exceptions.invalidArgumentsException:
		log.warning("{} tried to send an invalid message to {}".format(token.username, to))
		return 404


""" IRC-Bancho Connect/Disconnect/Join/Part interfaces"""
def fixUsernameForBancho(username):
	"""
	Convert username from IRC format (without spaces) to Bancho format (with spaces)

	:param username: username to convert
	:return: converted username
	"""
	# If there are no spaces or underscores in the name
	# return it
	if " " not in username and "_" not in username:
		return username

	# Exact match first
	result = glob.db.fetch("SELECT id FROM users WHERE username = %s LIMIT 1", [username])
	if result is not None:
		return username

	# Username not found, replace _ with space
	return username.replace("_", " ")

def fixUsernameForIRC(username):
	"""
	Convert an username from Bancho format to IRC format (underscores instead of spaces)

	:param username: username to convert
	:return: converted username
	"""
	return username.replace(" ", "_")

def IRCConnect(username):
	"""
	Handle IRC login bancho-side.
	Add token and broadcast login packet.

	:param username: username
	:return:
	"""
	userID = userUtils.getID(username)
	if not userID:
		log.warning("{} doesn't exist".format(username))
		return
	glob.tokens.deleteOldTokens(userID)
	glob.tokens.addToken(userID, irc=True)
	glob.streams.broadcast("main", serverPackets.userPanel(userID))
	log.info("{} logged in from IRC".format(username))

def IRCDisconnect(username):
	"""
	Handle IRC logout bancho-side.
	Remove token and broadcast logout packet.

	:param username: username
	:return:
	"""
	token = glob.tokens.getTokenFromUsername(username)
	if token is None:
		log.warning("{} doesn't exist".format(username))
		return
	logoutEvent.handle(token)
	log.info("{} disconnected from IRC".format(username))

def IRCJoinChannel(username, channel):
	"""
	Handle IRC channel join bancho-side.

	:param username: username
	:param channel: channel name
	:return: IRC return code
	"""
	userID = userUtils.getID(username)
	if not userID:
		log.warning("{} doesn't exist".format(username))
		return
	# NOTE: This should have also `toIRC` = False` tho,
	# since we send JOIN message later on ircserver.py.
	# Will test this later
	return joinChannel(userID, channel)

def IRCPartChannel(username, channel):
	"""
	Handle IRC channel part bancho-side.

	:param username: username
	:param channel: channel name
	:return: IRC return code
	"""
	userID = userUtils.getID(username)
	if not userID:
		log.warning("{} doesn't exist".format(username))
		return
	return partChannel(userID, channel)

def IRCAway(username, message):
	"""
	Handle IRC away command bancho-side.

	:param username:
	:param message: away message
	:return: IRC return code
	"""
	userID = userUtils.getID(username)
	if not userID:
		log.warning("{} doesn't exist".format(username))
		return
	glob.tokens.getTokenFromUserID(userID).awayMessage = message
	return 305 if message == "" else 306

