"""
IRCEvent and its subclasses.

Provides :py:data:`events`, which is a dict containing ``command: object`` values, where ``command`` is the IRC command the object should be called for.

Subclasses of :py:class:`IRCEvent` are classes that described IRC messages (which are defined by :rfc:`1459`).
"""

import re

from aleph.protocols.generic import GenericMessage

from data import Channel

events = dict() #: The events dictionary

def command (command=False, tag=False):
	"""
	A decorator that adds ``callable`` to the events dictionary.

	:Parameters:
		- `command`:	The IRC command the object is called for, defaulting to the objects name.
		- `tag` (str):	The tag to assign to the object, defaulting to the lowercased version of the objects name.
	"""
	def register (callable):
		events[str(command or callable.__name__)] = (tag or callable.__name__.lower(), callable)
		return callable
	return register

@command('default', tag='unknown')
class IRCEvent (object):
	""" Base class for IRC events """

	def __init__ (self, **kwargs):
		"""
		Creates a IRCEvent object from the given keyword args, which usually are the server and the results of :py:func:`parse_line`.

		:Parameters:
			- `handler` (Handler):	The handler the message originated from
			- `source` (Hostmask):	The source the message originated from
			- `command` (str):		The instruction of the message
			- `parameters` (list):	The parameters of the messages
			- `trailing` (str):		The data of the message
		"""
		self.__dict__.update(kwargs)

		if hasattr(self, 'init'):
			self.init()

	@property
	def nick (self):
		"""	Return the nickname of the source or None """
		return self.source.get('nick', None)

	def __str__ (self):
		"""	Default string representation of a IRCEvent object """
		return "{} {} {} :{}".format(self.source, self.command, self.parameters, self.trailing)

class IsMeMixin (object):
	 def isme (self):
		 return (self.source.name == self.handler.config['nick'])

"""	4.1 Connection Registration	"""

@command()
class QUIT (IRCEvent, IsMeMixin):
	"""
	A user has quit the server

		| *4.1.6 Quit*
		| QUIT :<reason>
	"""
	def __init__ (self, trailing, **kwargs):
		super(QUIT, self).__init__(**kwargs)
		self.reason = trailing

	def __str__ (self):
		return '{nick} [{host}] quit: {reason}'.format(
			nick=self.source.name, host=self.source, reason=self.reason)

""" 4.2 Channel operations """

@command()
class JOIN (IRCEvent, IsMeMixin):
	"""
	A user has joined a channel

	Create a local channel object if Aleph is joining,
	else add the name of the joing user to the channels names list

		| *4.2.1 Join message*
		| :nick JOIN #channel
	"""

	def __init__ (self, trailing, **message):
		super(JOIN, self).__init__(**message)
		self.channel = trailing

	def __str__ (self):
		return '{} [{}] joined {}'.format(self.source.name, self.source, self.channel)

@command()
class PART (IRCEvent, IsMeMixin):
	"""
	A user has left a channel

	Clear the local channel object if Aleph is parting,
	else remove the user's name from the channels names list.

		| *4.2.2 Part message*
		| :nick PART #channel
	"""
	def __init__ (self, trailing, **message):
		super(JOIN, self).__init__(**message)
		self.channel = trailing

	def __str__ (self):
		return '{} [{}] parted {}'.format(self.source.name, self.trailing)

@command()
class TOPIC (IRCEvent):
	"""
	A notification that a channel topic has changed

	| *4.2.4 Topic message*
	| :nick TOPIC #channel :New topic

	.. seealso:: :py:class:`RPL_TOPIC`, :py:func:`RPL_NOTOPIC`
	"""
	channel = property(lambda self: self.parameters[0])

	def init (self):
		self.get_channel(self.channel).topic = self.trailing

	def __str__ (self):
		return "{nick} set the topic for {channel}: {topic}".format(
			nick=self.source,
			channel=self.channel,
			topic=self.trailing
		)

""" 4.4 Sending messages """

class ChannelIRCEvent (IRCEvent):
	"""	Shared functionality for :py:class:`PRIVMSG` and :py:class:`NOTICE`. """

	def init (self):
		# Get Aleph's nickname
		nick = self.handler.config['nick']

		# Check if the message has been sent to Aleph directly,
		# or to a channel Aleph is in.
		if (self.receiver == nick):
			# The channel to reply to is the sender
			self.channel = self.source['nick']
			self.private = True
		else:
			# The channel to reply to is the channel that received the message
			self.channel = self.receiver
			self.private = False

	receiver = property(lambda self: self.parameters[0])

@command(tag='message')
class PRIVMSG (ChannelIRCEvent, GenericMessage):
	"""
	An incoming message

		| *4.4.1 Private messages*
		| Command: PRIVMSG
		| Parameters: <receiver> <text to be sent>

	``PRIVMSG.private``
		True for messages sent only to Aleph
	``PRIVMSG.aimed``
		True for messages aimed at Aleph (i.e. "Aleph: hello world").
		Aleph will strip the nickname from the message content.
	``PRIVMSG.text``
		Contains the message, with the prepending nick stripped if it was aimed at Aleph.
	"""

	def init (self):
		super(PRIVMSG, self).init()

		# If the message is aimed at Aleph, set ``aimed``
		# and remove the nick from the start of the text
		regex = "{}: (.*)".format(self.handler.config['nick'])
		match = re.match(regex, self.trailing)
		if match:
			self.aimed = True
			self.text = match.group(1)
		else:
			self.aimed = False
			self.text = self.trailing

	def msg (self, line):
		"""	Send a message to the channel the message originated from """
		return self.handler.privmsg(self.channel, line)

	def reply (self, line, aimed=False):
		"""
		Send a reply, prepending the senders nick to public messages

		:Parameters:
			- `aimed` (bool): Will allways aim the message at the sender if true.

		.. seealso:: :py:func:`EventIRCEvent.reply`
		"""
		if aimed or self.aimed:
			line = "{}: {}".format(self.source['nick'], line)
		return self.msg(line)

	#: Implements :py:func:`GenericMessage.message_text`.
	message_text = lambda self: self.text

	#: Implements :py:func:`GenericMessage.message_reply`.
	message_reply = lambda self, text: self.reply(text)

	def __str__ (self):
		return '{sender} messaged {receiver}: {text}'.format(
			sender   = self.source.name,
			receiver = 'me' if self.private else self.channel,
			text     = self.text,
		)

@command()
class NOTICE (ChannelIRCEvent):
	"""
	A notice - Aleph should *never* reply to notices,

	The IRC protocol declares that "automatic replies must never be
	sent in response to a NOTICE message".

		| *4.4.2 Notice*
		| Command: NOTICE
		| Parameters: <nickname> <text>
	"""

	def __str__ (self):
		return '{sender} notfied {receiver}: {text}'.format(
			sender=self.source.name, receiver=self.channel, text=self.trailing)

""" 6.2 Command responses """

# Topic

@command(331, tag=False)
def RPL_NOTOPIC (**kwargs):
	"""
	Marks the channel as having no topic

	Sets ``Channel.topic`` to an empty string
	and returns a `RPL_TOPIC` event

		| *331 RPL_NOTOPIC*
		| "<channel> :No topic is set"

	.. seealso:: :py:class:`TOPIC`, :py:class:`RPL_TOPIC`
	"""
	kwargs['trailing'] = ""
	return RPL_TOPIC(**kwargs)

@command(332, tag='topic')
class RPL_TOPIC (IRCEvent):
	"""
	The topic for a channel

	Sets ``Channel.topic`` to the new topic,
	or to ``""`` if created by `RPL_NOTOPIC`.

		| *332 RPL_TOPIC*
		| "<channel> :<topic>"

	.. seealso:: :py:class:`TOPIC`, :py:func:`RPL_NOTOPIC`
	"""

	def __init__ (self, handler, parameters, trailing, **message):
		self.handler  = handler
		self.channel = parameters[1]
		self.topic   = trailing

	def __str__ (self):
		return 'The topic for {self.channel} is: {self.topic}'.format(self=self)

@command(333, tag='topic-meta')
class RPL_TOPIC_META (IRCEvent):
	"""
	The metadata for a channel topic

	There appears to be no mention of this in the IRC RFC's.

	.. seealso:: :py:class:`TOPIC`
	"""
	def __init__ (self, parameters, trailing, **message):
		super(RPL_TOPIC_META, self).__init__(parameters=parameters[0:1], **message)
		self.channel, self.topic_setter, self.time = parameters[1:4]

	def __str__ (self):
		return 'The topic for {channel} was set by {nick} at {time}.'.format(
			channel=self.channel, nick=self.topic_setter, time=self.time)

# Names

@command(353, tag=False)
def RPL_NAMREPLY (handler, parameters, trailing, **message):
	"""
	Part or all of the names list for a channel

	If the RPL_NAMREPLY flag is not set on the channel, this is the
	first set of names - the local names list should be cleared and
	replaced with the canonical version.

	The names in the message are added to the channels local names list

		| *353 RPL_NAMREPLY*
		| "<channel> :[[@|+]<nick> [[@|+]<nick> [...]]]"
	"""
	channel = parameters[2]
	if not channel in handler.tmp:
		handler.tmp[channel] = list()
	handler.tmp[channel].extend(trailing.split())

@command(366, tag='names')
class RPL_ENDOFNAMES (IRCEvent):
	"""
	Marks the end of the names list

	Remove the RPL_NAMREPLY flag from the channel

		| *366 RPL_ENDOFNAMES*
		| "<channel> :End of /NAMES list"
	"""
	def __init__ (self, handler, parameters, **message):
		super(RPL_ENDOFNAMES, self).__init__(handler=handler, parameters=parameters[0:1], **message)

		#: The name of the channel
		self.channel = parameters[1]

		#: A list of the names in the channel
		self.names = handler.tmp[self.channel]

		del handler.tmp[self.channel]

	def __str__ (self):
		return "Users in {channel}: {names}".format(
			channel=self.channel, names=', '.join(self.names))

# IRCEvent of the day

@command(375, tag=False)
def RPL_MOTDSTART (handler, **kwargs):
	"""
	Marks the start of the servers MOTD

	Sets the local MOTD to ``""``

		| *375 RPL_MOTDSTART*
		| ":- <server> IRCEvent of the day - "
	"""
	handler.tmp['motd'] = ""

@command(372, tag=False)
def RPL_MOTD (handler, trailing, **kwargs):
	"""
	A line from the MOTD, which should be appended to the current MOTD

		| *372 RPL_MOTD*
		| ":- <text>"
	"""
	handler.tmp['motd'] += trailing[2:] + "\n"

@command(376, tag='motd')
class RPL_ENDOFMOTD (IRCEvent):
	"""
	Marks the end of the server MOTD

	Strips the trailing newline from the MOTD

		| *376 RPL_ENDOFMOTD*
		| ":End of /MOTD command"
	"""

	def __init__ (self, **message):
		""" Strip the trailing \n from the MOTD """
		super(RPL_ENDOFMOTD, self).__init__(**message)

		#: The server's MOTD
		self.motd = self.handler.tmp['motd'][:-1]

	def __str__ (self):
		return self.motd
