"""
IMC2 client module. Handles connecting to and communicating with an IMC2 server.
"""

from time import time
from twisted.internet import task
from twisted.application import internet
from twisted.internet import protocol
from twisted.conch import telnet

from evennia.server.session import Session
from evennia.utils import logger, utils
from evennia.server.portal.imc2lib import imc2_ansi
from evennia.server.portal.imc2lib import imc2_packets as pck

from django.utils.translation import ugettext as _


# storage containers for IMC2 muds and channels

class IMC2Mud(object):
    """
    Stores information about other games connected to our current IMC2 network.
    """
    def __init__(self, packet):
        self.name = packet.origin
        self.versionid = packet.optional_data.get('versionid', None)
        self.networkname = packet.optional_data.get('networkname', None)
        self.url = packet.optional_data.get('url', None)
        self.host = packet.optional_data.get('host', None)
        self.port = packet.optional_data.get('port', None)
        self.sha256 = packet.optional_data.get('sha256', None)
        # This is used to determine when a Mud has fallen into inactive status.
        self.last_updated = time()


class IMC2MudList(dict):
    """
    Keeps track of other MUDs connected to the IMC network.
    """
    def get_mud_list(self):
        """
        Returns a sorted list of connected Muds.
        """
        muds = self.items()
        muds.sort()
        return [value for key, value in muds]

    def update_mud_from_packet(self, packet):
        """
        This grabs relevant info from the packet and stuffs it in the
        Mud list for later retrieval.
        """
        mud = IMC2Mud(packet)
        self[mud.name] = mud

    def remove_mud_from_packet(self, packet):
        """
        Removes a mud from the Mud list when given a packet.
        """
        mud = IMC2Mud(packet)
        try:
            del self[mud.name]
        except KeyError:
            # No matching entry, no big deal.
            pass


class IMC2Channel(object):
    """
    Stores information about channels available on the network.
    """
    def __init__(self, packet):
        self.localname = packet.optional_data.get('localname', None)
        self.name = packet.optional_data.get('channel', None)
        self.level = packet.optional_data.get('level', None)
        self.owner = packet.optional_data.get('owner', None)
        self.policy = packet.optional_data.get('policy', None)
        self.last_updated = time()


class IMC2ChanList(dict):
    """
    Keeps track of Channels on the IMC network.
    """

    def get_channel_list(self):
        """
        Returns a sorted list of cached channels.
        """
        channels = self.items()
        channels.sort()
        return [value for key, value in channels]

    def update_channel_from_packet(self, packet):
        """
        This grabs relevant info from the packet and stuffs it in the
        channel list for later retrieval.
        """
        channel = IMC2Channel(packet)
        self[channel.name] = channel

    def remove_channel_from_packet(self, packet):
        """
        Removes a channel from the Channel list when given a packet.
        """
        channel = IMC2Channel(packet)
        try:
            del self[channel.name]
        except KeyError:
            # No matching entry, no big deal.
            pass


#
# IMC2 protocol
#

class IMC2Bot(telnet.StatefulTelnetProtocol, Session):
    """
    Provides the abstraction for the IMC2 protocol. Handles connection,
    authentication, and all necessary packets.
    """
    def __init__(self):
        self.is_authenticated = False
        # only support plaintext passwords
        self.auth_type = "plaintext"
        self.sequence = None
        self.imc2_mudlist = IMC2MudList()
        self.imc2_chanlist = IMC2ChanList()

    def _send_packet(self, packet):
        "Helper function to send packets across the wire"
        packet.imc2_protocol = self
        packet_str = utils.to_str(packet.assemble(self.factory.mudname,
                         self.factory.client_pwd, self.factory.server_pwd))
        self.sendLine(packet_str)

    def _isalive(self):
        "Send an isalive packet"
        self._send_packet(pck.IMC2PacketIsAlive())

    def _keepalive(self):
        "Send a keepalive packet"
        # send to channel?
        self._send_packet(pck.IMC2PacketKeepAliveRequest())

    def _channellist(self):
        "Sync the network channel list"
        checked_networks = []
        if not self.network in checked_networks:
            self._send_packet(pck.IMC2PacketIceRefresh())
            checked_networks.append(self.network)

    def _prune(self):
        "Prune active channel list"
        t0 = time()
        for name, mudinfo in self.imc2_mudlist.items():
            if t0 - mudinfo.last_updated > 3599:
                del self.imc2_mudlist[name]

    def _whois_reply(self, packet):
        "handle reply from server from an imcwhois request"
        # packet.target potentially contains the id of an character to target
        # not using that here
        response_text = imc2_ansi.parse_ansi(packet.optional_data.get('text', 'Unknown'))
        string = _('Whois reply from %(origin)s: %(msg)s') % {"origin":packet.origin, "msg":response_text}
        # somehow pass reply on to a given player, for now we just send to channel
        self.data_in(string)

    def _format_tell(self, packet):
        """
        Handle tells over IMC2 by formatting the text properly
        """
        return _("{c%(sender)s@%(origin)s{n {wpages (over IMC):{n %(msg)s") % {"sender": packet.sender,
                                                        "origin": packet.origin,
                                                        "msg": packet.optional_data.get('text', 'ERROR: No text provided.')}

    def _imc_login(self, line):
        "Connect and identify to imc network"

        if self.auth_type == "plaintext":
            # Only support Plain text passwords.
            # SERVER Sends: PW <servername> <serverpw> version=<version#> <networkname>

            logger.log_infomsg("IMC2: AUTH< %s" % line)

            line_split = line.split(' ')
            pw_present = line_split[0] == 'PW'
            autosetup_present = line_split[0] == 'autosetup'

            if "reject" in line_split:
                auth_message = _("IMC2 server rejected connection.")
                logger.log_infomsg(auth_message)
                return

            if pw_present:
                self.server_name = line_split[1]
                self.network_name = line_split[4]
            elif autosetup_present:
                logger.log_infomsg(_("IMC2: Autosetup response found."))
                self.server_name = line_split[1]
                self.network_name = line_split[3]
            self.is_authenticated = True
            self.sequence = int(time())

            # Log to stdout and notify over MUDInfo.
            logger.log_infomsg('IMC2: Authenticated to %s' % self.factory.network)

            # Ask to see what other MUDs are connected.
            self._send_packet(pck.IMC2PacketKeepAliveRequest())
            # IMC2 protocol states that KeepAliveRequests should be followed
            # up by the requester sending an IsAlive packet.
            self._send_packet(pck.IMC2PacketIsAlive())
            # Get a listing of channels.
            self._send_packet(pck.IMC2PacketIceRefresh())

    def connectionMade(self):
        """
        Triggered after connecting to the IMC2 network.
        """

        self.stopping = False
        self.factory.bot = self
        address = "%s@%s" % (self.mudname, self.network)
        self.init_session("ircbot", address, self.factory.sessionhandler)
        # link back and log in
        self.uid = int(self.factory.uid)
        self.logged_in = True
        self.factory.sessionhandler.connect(self)
        logger.log_infomsg("IMC2 bot connected to %s." % self.network)
        # Send authentication packet. The reply will be caught by lineReceived
        self._send_packet(pck.IMC2PacketAuthPlaintext())

    def lineReceived(self, line):
        """
        IMC2 -> Evennia

        Triggered when text is received from the IMC2 network. Figures out
        what to do with the packet. This deals with the following

        """
        line = line.strip()

        if not self.is_authenticated:
            # we are not authenticated yet. Deal with this.
            self._imc_login(line)
            return

        #logger.log_infomsg("IMC2: RECV> %s" % line)

        # Parse the packet and encapsulate it for easy access
        packet = pck.IMC2Packet(self.mudname, packet_str=line)

        # Figure out what kind of packet we're dealing with and hand it
        # off to the correct handler.

        if packet.packet_type == 'is-alive':
            self.imc2_mudlist.update_mud_from_packet(packet)
        elif packet.packet_type == 'keepalive-request':
            # Don't need to check the destination, we only receive these
            # packets when they are intended for us.
            self.send_packet(pck.IMC2PacketIsAlive())
        elif packet.packet_type == 'ice-msg-b':
            self.data_out(text=line, packettype="broadcast")
        elif packet.packet_type == 'whois-reply':
            # handle eventual whois reply
            self._whois_reply(packet)
        elif packet.packet_type == 'close-notify':
            self.imc2_mudlist.remove_mud_from_packet(packet)
        elif packet.packet_type == 'ice-update':
            self.imc2_chanlist.update_channel_from_packet(packet)
        elif packet.packet_type == 'ice-destroy':
            self.imc2_chanlist.remove_channel_from_packet(packet)
        elif packet.packet_type == 'tell':
            # send message to identified player
            pass

    def data_in(self, text=None, **kwargs):
        """
        Data IMC2 -> Evennia
        """
        text = "bot_data_in " + text
        self.sessionhandler.data_in(self, text=text, **kwargs)

    def data_out(self, text=None, **kwargs):
        """
        Evennia -> IMC2

        Keywords
           packet_type:
            broadcast - send to everyone on IMC channel
            tell - send a tell (see target keyword)
            whois - get whois information (see target keyword)
          sender - used by tell to identify the sender
          target - key identifier of target to tells or whois. If not
                   given "Unknown" will be used.
          destination - used by tell to specify mud destination to send to

        """

        if self.sequence:
            # This gets incremented with every command.
            self.sequence += 1

        packet_type = kwargs.get("packet_type", "imcbroadcast")

        if packet_type == "broadcast":
            # broadcast to everyone on IMC channel

            if text.startswith("bot_data_out"):
                text = text.split(" ", 1)[1]
            else:
                return

            # we remove the extra channel info since imc2 supplies this anyway
            if ":" in text:
                header, message = [part.strip() for part in text.split(":", 1)]
            # Create imc2packet and send it
            self._send_packet(pck.IMC2PacketIceMsgBroadcasted(self.servername,
                                                        self.channel,
                                                        header, text))
        elif packet_type == "tell":
            # send an IMC2 tell
            sender = kwargs.get("sender", self.mudname)
            target = kwargs.get("target", "Unknown")
            destination = kwargs.get("destination", "Unknown")
            self._send_packet(pck.IMC2PacketTell(sender, target, destination, text))

        elif packet_type == "whois":
            # send a whois request
            sender = kwargs.get("sender", self.mudname)
            target = kwargs.get("target", "Unknown")
            self._send_packet(pck.IMC2PacketWhois(sender, target))


class IMC2BotFactory(protocol.ReconnectingClientFactory):
    """
    Creates instances of the IMC2Protocol. Should really only ever
    need to create one connection. Tied in via evennia/server.py.
    """
    initialDelay = 1
    factor = 1.5
    maxDelay = 60

    def __init__(self, sessionhandler, uid=None, network=None, channel=None,
                 port=None, mudname=None, client_pwd=None, server_pwd=None):
        self.uid = uid
        self.network = network
        sname, host = network.split(".", 1)
        self.servername = sname.strip()
        self.channel = channel
        self.port = port
        self.mudname = mudname
        self.protocol_version = '2'
        self.client_pwd = client_pwd
        self.server_pwd = server_pwd
        self.bot = None
        self.task_isalive = None
        self.task_keepalive = None
        self.task_prune = None
        self.task_channellist = None

    def buildProtocol(self, addr):
        "Build the protocol"
        protocol = IMC2Bot()
        protocol.factory = self
        protocol.network = self.network
        protocol.servername = self.servername
        protocol.channel = self.channel
        protocol.mudname = self.mudname
        protocol.port = self.port
        return protocol

    def clientConnectionFailed(self, connector, reason):
        self.retry(connector)

    def clientConnectionLost(self, connector, reason):
        if not self.bot.stopping:
            self.retry(connector)

    def start(self):
        "Connect session to sessionhandler"
        def errback(fail):
            logger.log_errmsg(fail.value)

        if self.port:
            service = internet.TCPClient(self.network, int(self.port), self)
            self.sessionhandler.portal.services.addService(service)
        # start tasks
        self.task_isalive = task.LoopingCall(self.bot._isalive)
        self.task_keepalive = task.LoopingCall(self.bot._keepalive)
        self.task_prune = task.LoopingCall(self.bot._prune)
        self.task_channellist = task.LoopingCall(self.bot._channellist)
        self.task_isalive.start(900, now=False)
        self.task_keepalive.start(3500, now=False)
        self.task_prune.start(1800, now=False)
        self.task_channellist.start(3600 * 24, now=False)

