"""
This module implements the telnet protocol.

This depends on a generic session module that implements
the actual login procedure of the game, tracks
sessions etc.

"""

import re
from twisted.conch.telnet import Telnet, StatefulTelnetProtocol, IAC, LINEMODE, GA, WILL, WONT, ECHO
from evennia.server.session import Session
from evennia.server.portal import ttype, mssp, msdp, naws
from evennia.server.portal.mccp import Mccp, mccp_compress, MCCP
from evennia.server.portal.mxp import Mxp, mxp_parse
from evennia.utils import utils, ansi, logger

_RE_N = re.compile(r"\{n$")
_RE_LEND = re.compile(r"\n$|\r$", re.MULTILINE)

class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
    """
    Each player connecting over telnet (ie using most traditional mud
    clients) gets a telnet protocol instance assigned to them.  All
    communication between game and player goes through here.
    """
    def connectionMade(self):
        """
        This is called when the connection is first
        established.
        """
        # initialize the session
        self.iaw_mode = False
        self.no_lb_mode = False
        client_address = self.transport.client
        # this number is counted down for every handshake that completes.
        # when it reaches 0 the portal/server syncs their data
        self.handshakes = 6 # naws, ttype, mccp, mssp, msdp, mxp
        self.init_session("telnet", client_address, self.factory.sessionhandler)

        # negotiate client size
        self.naws = naws.Naws(self)
        # negotiate ttype (client info)
        # Obs: mudlet ttype does not seem to work if we start mccp before ttype. /Griatch
        self.ttype = ttype.Ttype(self)
        # negotiate mccp (data compression) - turn this off for wireshark analysis
        self.mccp = Mccp(self)
        # negotiate mssp (crawler communication)
        self.mssp = mssp.Mssp(self)
        # msdp
        self.msdp = msdp.Msdp(self)
        # mxp support
        self.mxp = Mxp(self)
        # keepalive watches for dead links
        self.transport.setTcpKeepAlive(1)
        # add this new connection to sessionhandler so
        # the Server becomes aware of it.
        self.sessionhandler.connect(self)

        # timeout the handshakes in case the client doesn't reply at all
        from evennia.utils.utils import delay
        delay(2, callback=self.handshake_done, retval=True)

    def handshake_done(self, force=False):
        """
        This is called by all telnet extensions once they are finished.
        When all have reported, a sync with the server is performed.
        The system will force-call this sync after a small time to handle
        clients that don't reply to handshakes at all.
        info - debug text from the protocol calling
        """
        if self.handshakes > 0:
            if force:
                self.sessionhandler.sync(self)
                return
            self.handshakes -= 1
            if self.handshakes <= 0:
                # do the sync
                self.sessionhandler.sync(self)

    def enableRemote(self, option):
        """
        This sets up the remote-activated options we allow for this protocol.
        """
        pass
        return (option == LINEMODE or
                option == ttype.TTYPE or
                option == naws.NAWS or
                option == MCCP or
                option == mssp.MSSP)

    def enableLocal(self, option):
        """
        Call to allow the activation of options for this protocol
        """
        return (option == MCCP or option==ECHO)

    def disableLocal(self, option):
        """
        Disable a given option
        """
        if option == ECHO:
            return True
        if option == MCCP:
            self.mccp.no_mccp(option)
            return True
        else:
            return super(TelnetProtocol, self).disableLocal(option)

    def connectionLost(self, reason):
        """
        this is executed when the connection is lost for
        whatever reason. it can also be called directly, from
        the disconnect method
        """
        self.sessionhandler.disconnect(self)
        self.transport.loseConnection()

    def dataReceived(self, data):
        """
        This method will split the incoming data depending on if it
        starts with IAC (a telnet command) or not. All other data will
        be handled in line mode. Some clients also sends an erroneous
        line break after IAC, which we must watch out for.

        OOB protocols (MSDP etc) already intercept subnegotiations
        on their own, never entering this method. They will relay
        their parsed data directly to self.data_in.

        """

        if data and data[0] == IAC or self.iaw_mode:
            try:
                #print "IAC mode"
                super(TelnetProtocol, self).dataReceived(data)
                if len(data) == 1:
                    self.iaw_mode = True
                else:
                    self.iaw_mode = False
                return
            except Exception, err1:
                conv = ""
                try:
                    for b in data:
                        conv += " " + repr(ord(b))
                except Exception, err2:
                    conv = str(err2) + ":", str(data)
                out = "Telnet Error (%s): %s (%s)" % (err1, data, conv)
                logger.log_trace(out)
                return

        if self.no_lb_mode and _RE_LEND.match(data):
            # we are in no_lb_mode and we get a single line break
            # - this line break should have come with the previous
            # command - it was already added so we drop it here
            self.no_lb_mode = False
            return
        elif not _RE_LEND.search(data):
            # no line break at the end of the command, note this.
            data = data.rstrip("\r\n") + "\n"
            self.no_lb_mode = True

        # if we get to this point the command should end with a linebreak.
        # We make sure to add it, to fix some clients messing this up.
        #print "line data in:", repr(data)
        StatefulTelnetProtocol.dataReceived(self, data)

    def _write(self, data):
        "hook overloading the one used in plain telnet"
        # print "_write (%s): %s" % (self.state,  " ".join(str(ord(c)) for c in data))
        data = data.replace('\n', '\r\n').replace('\r\r\n', '\r\n')
        #data = data.replace('\n', '\r\n')
        super(TelnetProtocol, self)._write(mccp_compress(self, data))

    def sendLine(self, line):
        "hook overloading the one used by linereceiver"
        #print "sendLine (%s):\n%s" % (self.state, line)
        #escape IAC in line mode, and correctly add \r\n
        line += self.delimiter
        line = line.replace(IAC, IAC + IAC).replace('\n', '\r\n')
        return self.transport.write(mccp_compress(self, line))

    def lineReceived(self, string):
        """
        Telnet method called when data is coming in over the telnet
        connection. We pass it on to the game engine directly.
        """
        self.data_in(text=string)

    # Session hooks

    def disconnect(self, reason=None):
        """
        generic hook for the engine to call in order to
        disconnect this protocol.
        """
        if reason:
            self.data_out(reason)
        self.connectionLost(reason)

    def data_in(self, text=None, **kwargs):
        """
        Data Telnet -> Server
        """
        self.sessionhandler.data_in(self, text=text, **kwargs)

    def data_out(self, text=None, **kwargs):
        """
        Data Evennia -> Player.
        generic hook method for engine to call in order to send data
        through the telnet connection.

        valid telnet kwargs:
            oob=<string> - supply an Out-of-Band instruction.
            xterm256=True/False - enforce xterm256 setting. If not
                                  given, ttype result is used. If
                                  client does not suport xterm256, the
                                  ansi fallback will be used
            mxp=True/False - enforce mxp setting. If not given, enables if we
                             detected client support for it
            ansi=True/False - enforce ansi setting. If not given,
                              ttype result is used.
            nomarkup=True - strip all ansi markup (this is the same as
                            xterm256=False, ansi=False)
            raw=True - pass string through without any ansi
                       processing (i.e. include Evennia ansi markers but do
                       not convert them into ansi tokens)
            prompt=<string> - supply a prompt text which gets sent without a
                              newline added to the end
            echo=True/False
        The telnet ttype negotiation flags, if any, are used if no kwargs
        are given.
        """
        try:
            text = utils.to_str(text if text else "", encoding=self.encoding)
        except Exception, e:
            self.sendLine(str(e))
            return
        if "oob" in kwargs:
            oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob"))
            if "MSDP" in self.protocol_flags:
                for cmdname, args, kwargs in oobstruct:
                    #print "cmdname, args, kwargs:", cmdname, args, kwargs
                    msdp_string = self.msdp.evennia_to_msdp(cmdname, *args, **kwargs)
                    #print "msdp_string:", msdp_string
                    self.msdp.data_out(msdp_string)

        # parse **kwargs, falling back to ttype if nothing is given explicitly
        ttype = self.protocol_flags.get('TTYPE', {})
        xterm256 = kwargs.get("xterm256", ttype.get('256 COLORS', False) if ttype.get("init_done") else True)
        useansi = kwargs.get("ansi", ttype and ttype.get('ANSI', False) if ttype.get("init_done") else True)
        raw = kwargs.get("raw", False)
        nomarkup = kwargs.get("nomarkup", not (xterm256 or useansi))
        prompt = kwargs.get("prompt")
        echo = kwargs.get("echo", None)
        mxp = kwargs.get("mxp", self.protocol_flags.get("MXP", False))

        #print "telnet kwargs=%s, message=%s" % (kwargs, text)
        #print "xterm256=%s, useansi=%s, raw=%s, nomarkup=%s, init_done=%s" % (xterm256, useansi, raw, nomarkup, ttype.get("init_done"))
        if raw:
            # no processing whatsoever
            self.sendLine(text)
        elif text:
            # we need to make sure to kill the color at the end in order
            # to match the webclient output.
            #print "telnet data out:", self.protocol_flags, id(self.protocol_flags), id(self), "nomarkup: %s, xterm256: %s" % (nomarkup, xterm256)
            linetosend = ansi.parse_ansi(_RE_N.sub("", text) + "{n", strip_ansi=nomarkup, xterm256=xterm256, mxp=mxp)
            if mxp:
                linetosend = mxp_parse(linetosend)
            self.sendLine(linetosend)

        if prompt:
            # Send prompt separately
            prompt = ansi.parse_ansi(_RE_N.sub("", prompt) + "{n", strip_ansi=nomarkup, xterm256=xterm256)
            if mxp:
                prompt = mxp_parse(prompt)
            prompt = prompt.replace(IAC, IAC + IAC).replace('\n', '\r\n')
            prompt += IAC + GA
            self.transport.write(mccp_compress(self, prompt))
        if echo:
            self.transport.write(mccp_compress(self, IAC+WONT+ECHO))
        elif echo == False:
            self.transport.write(mccp_compress(self, IAC+WILL+ECHO))

