#!/usr/bin/env python3

import sys
import re

from twisted.internet import defer
from twisted.internet import protocol
from twisted.internet import reactor
from twisted.python import log
from twisted.python.logfile import DailyLogFile

# from twisted.enterprise import adbapi
import pendulum
from subprocess import check_output

# This isn't the best configuration, but it's simple
# and works.  Mostly.

try:
    from config_dev import *
except ModuleNotFoundError:
    from config import *

# from config import *

# Connect to:
# HOST = "twgs"
# PORT = 2002
# Listen on:
# LISTEN_PORT = 2002
# LISTEN_ON = "0.0.0.0"

version = check_output(
    [
        "git",
        "describe",
        "--long",
        "--tags",
        #        "--dirty",
        "--always",
        "--match",
        "v[0-9]\.[0-9]\.[0-9]",
    ],
    universal_newlines=True,
).strip()

# Cleans all ANSI
cleaner = re.compile(r"\x1b\[[0-9;]*[A-Zmh]")
# Looks for ANSI (that should be considered to be a newline)
makeNL = re.compile(r"\x1b\[[0-9;]*[J]")


def treatAsNL(line):
    """ Replace any ANSI codes that would be better understood as newlines. """
    global makeNL
    return makeNL.sub("\n", line)


def cleanANSI(line):
    """ Remove all ANSI codes. """
    global cleaner
    return cleaner.sub("", line)
    # return re.sub(r'\x1b\[([0-9,A-Z]{1,2}(;[0-9]{1,2})?(;[0-9]{3})?)?[m|K]?', '', line)


class Game(protocol.Protocol):
    def __init__(self):
        # user is rlogin username
        self.user = None
        # buffer is used to capture the rlogin username
        self.buffer = ""

    def connectionMade(self):
        log.msg("Client: connected to peer")
        self.queue_player = self.factory.queue_player
        self.queue_game = self.factory.queue_game
        self.setPlayerReceived()
        # self.queue_twgs.get().addCallback(self.serverDataReceived)

    def setPlayerReceived(self):
        """ Get deferred from client queue, callback clientDataReceived. """
        self.queue_player.get().addCallback(self, playerDataReceived)

    def playerDataReceived(self, chunk):
        # rlogin looks like this:  \x00 password \x00 username \x00 terminal/speed \x00
        # b'\x00up2lat3\x00bugz\x00ansi-bbs/115200\x00'
        # We're looking for 4 \x00!
        # TODO: Line processing, and line cleaning (remove ANSI color codes)
        if chunk is False:
            self.queue_twgs = None
            log.msg("Client: disconnecting from peer")
            self.factory.continueTrying = False
            self.transport.loseConnection()
        else:
            if self.user is None:
                # Decode the rlogin data
                self.buffer += chunk.decode("utf-8", "ignore")

                # Ok, process this
                # self.buffer += chunk.decode('utf-8')
                # We don't have the username yet
                parts = self.buffer.split("\x00")
                if len(parts) >= 5:
                    # Got it!
                    self.user = parts[1]
                    log.msg("User: {0}".format(self.user))
                    # Reset buffer -- remove everything before last \x00
                    zpos = self.buffer.rindex("\x00")
                    self.buffer = self.buffer[zpos + 1 :]
                    self.buffer = ""
                    # init sqlite db using the username
                    self.factory.getUser(self.user)

            # Pass received data to the server
            self.transport.write(chunk)
            self.setPlayerReceived()
            # self.queue_twgs.get().addCallback(self.serverDataReceived)

    def dataReceived(self, chunk):
        # log.msg("Client: %d bytes received from peer" % len(chunk))
        # clean, strip ANSI, etc.

        # log.msg("<<", chunk.decode("utf-8", "ignore"))
        # log.msg("<<", repr(chunk))
        # self.factory.queue_client.put(chunk)
        # self.queue_game.put(chunk)
        self.queue_player.put(chunk)

    def connectionLost(self, why):
        log.msg("Game connectionLost because: %s" % why)
        self.queue_game.put(False)

        # self.factory.queue_client.put(False)
        # self.queue_twgs = None
        self.transport.loseConnection()


class GlueFactory(protocol.ClientFactory):
    # class GlueFactory(protocol.Factory):
    maxDelay = 10
    protocol = Game

    def __init__(self, twgs):
        self.twgs = twgs
        self.queue_player = twgs.queue_player
        self.queue_game = twgs.queue_game

    def closeIt(self):
        log.msg("closeIt")
        self.queue_player.put(False)

    def getUser(self, user):
        log.msg("getUser( %s )" % user)
        self.twgs.logUser(user)

    # This was needed when I replaced ClientFactory with Factory.
    # def clientConnectionLost(self, connector, why):
    #     log.msg("clientconnectionlost: %s" % why)
    #     self.queue_client.put(False)

    def clientConnectionFailed(self, connector, why):
        log.msg("connection to game failed: %s" % why)
        self.queue_player.put(b"Sorry!  I'm Unable to connect to the game server.\r\n")

        # syncterm gets cranky/locks up if we close this here.
        # (Because it is still sending rlogin information?)
        reactor.callLater(2, self.closeIt)


class Player(protocol.Protocol):
    def __init__(self):
        self.buffer = ""
        self.fpRaw = None
        self.fpLines = None
        self.action = None
        self.username = ""
        self.game = ""
        self.passon = True

    def connectionMade(self):
        """ connected, setup queues. """
        self.queue_player = defer.DeferredQueue()
        self.queue_game = defer.DeferredQueue()
        self.setGameReceived()
        # self.action = ProxyAction(self)

        factory = GlueFactory(self)
        # Make connection to the game server
        reactor.connectTCP(HOST, PORT, factory, 5)

    def setGameReceived(self):
        """ Get deferred from client queue, callback clientDataReceived. """
        self.queue_game.get().addCallback(self.gameDataReceived)

    def logUser(self, user):
        """ We have the username. """
        now = pendulum.now()
        self.username = user
        filename = now.format("YYYY-MM-DD_HHmm") + "-" + user.lower()

        # Are we saving RAW output?
        if RAW:
            self.fpRaw = open(filename + ".raw", "ab")
        self.fpLines = open(filename + ".lines", "a")
        print("Log created:", now.to_rss_string(), "\n", file=self.fpLines)

    def setGame(self, game):
        """ We have the game (A-P) they are playing. """
        if self.game != game:
            log.msg("USER {0} ENTERED {1}".format(self.username, self.game))
            self.data = {}
        self.game = game

    def gotLine(self, line):
        """ We got a line from the server.

        This is ANSI filtered.  
        Backspaces have removed the character from the line.
        The line is unicode.  We don't need to decode it. ;)

        """
        log.msg(">>> [{0}]".format(line))
        if self.fpLines is not None:
            print(line, file=self.fpLines)

        if "TWGS v2.20b" in line:
            if "www.eisonline.com" in line:
                # Must not be unicode

                # Is there a way to NOT have this logged?
                self.queue_client.put(
                    (
                        b"TWGS Proxy build "
                        + version.encode()
                        + b" is active. \x1b[1;34m~\x1b[0m to activate.\n\r\n\r",
                    )
                )
        if "TradeWars Game Server" in line and "Copyright (C) EIS" in line:
            # We are not in a game
            self.game = ""
        if "Selection (? for menu): " in line:
            game = line[-1]
            if game >= "A" and game < "Q":
                self.setGame(game)
        # If we're not passing it on to the user, we better be looking at it.
        if self.action and not self.passon:
            self.action.server(line)

    def gameDataReceived(self, chunk):
        """ Data received from the client/player. """
        if chunk is False:
            self.transport.loseConnection()
        else:
            if type(chunk) is tuple:
                # Special case where we want to send something to the user
                # but not have it logged.
                self.transport.write(chunk[0])
                self.setGameReceived()
            else:
                if self.fpRaw is not None:
                    self.fpRaw.write(chunk)
                self.buffer += chunk.decode("utf-8", "ignore")

                # Process any backspaces in the buffer

                while "\x08" in self.buffer:
                    part = self.buffer.partition("\x08")
                    self.buffer = part[0][:-1] + part[2]

                # Treat some ANSI codes as a newline (for purposes of Lines)
                self.buffer = treatAsNL(self.buffer)
                # I think I need something else in here.  When something enters or leaves the sector
                # The message isn't shown on it's own line.  I think they are using ANSI codes to
                # clear the line.  (Which certainly would be faster!)

                # Break the buffer into lines

                while "\n" in self.buffer:
                    part = self.buffer.partition("\n")
                    line = part[0].replace("\r", "")
                    # Clean ANSI codes from the line
                    line = cleanANSI(line)
                    self.gotLine(line)
                    self.buffer = part[2]

                # log.msg("Server: writing %d bytes to original client" % len(chunk))
                if self.passon:
                    self.transport.write(chunk)

                self.setGameReceived()

    def dataReceived(self, chunk):
        if self.action and self.action.isActive():
            # Do something completely different here
            self.action.received(chunk)
        else:
            # Did player activate hotkey?
            if chunk == b"~":
                self.action.activate(self.buffer)
            else:
                self.queue_game.put(chunk)

    def connectionLost(self, why):
        log.msg("lost connection %s" % why)
        self.queue_game.put(False)

        # Close log files, if open
        if self.fpRaw is not None:
            self.fpRaw.close()
            self.fpRaw = None
        if self.fpLines is not None:
            if self.buffer != "":
                print(self.buffer, file=self.fpLines)
            self.fpLines.close()
            self.fpLines = None

    def connectionFailed(self, why):
        log.msg("connectionFailed: %s" % why)


if __name__ == "__main__":
    log.startLogging(DailyLogFile("proxy.log", "."))
    log.msg("This is version: %s" % version)
    factory = protocol.Factory()
    factory.protocol = Player
    reactor.listenTCP(LISTEN_PORT, factory, interface=LISTEN_ON)
    reactor.run()