#!/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 *

# Extract the version information from git.
# The match gives us only tags starting with v[0-9]*  Using anything else trips up on double digits.
version = check_output(
    [
        "git",
        "describe",
        "--abbrev=8",
        "--long",
        "--tags",
        "--dirty",
        "--always",
        "--match",
        "v[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)
# This needs to see what is send when something enters / leaves
# the player's current sector.  (That doesn't work/isn't
# detected.  NNY!)
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 Observer(object):
    def __init__(self):
        self.dispatch = {}

    def emit(signal, message):
        if signal in dispatch:
            # something to do
            for listener in dispatch[signal]:
                reactor.callLater(0, listener, message)
            return True
        return False

    def connect(signal, func):
        if not signal in dispatch:
            dispatch[signal] = []

        dispatch[signal].append(func)

    def disconnect(signal, func):
        if signal in dispatch:
            dispatch[signal].remove(func)


class Game(protocol.Protocol):
    def __init__(self):
        self.buffer = ""
        self.game = ""

    def connectionMade(self):
        log.msg("Connected to Game Server")
        self.queue_player = self.factory.queue_player
        self.queue_game = self.factory.queue_game
        self.observer = self.factory.observer
        self.setPlayerReceived()

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

    def playerDataReceived(self, chunk):
        if chunk is False:
            self.queue_player = None
            log.msg("Player: disconnected, close connection to game")
            # I don't believe I need this if I'm using protocol.Factory
            self.factory.continueTrying = False
            self.transport.loseConnection()
        else:
            # Pass received data to the server
            self.transport.write(chunk)
            self.setPlayerReceived()

    def lineReceived(self, line):
        """ line received from the game. """
        log.msg(">> [{0}]".format(line))
        if "TWGS v2.20b" in line and "www.eisonline.com" in line:
            # Must not be unicode

            # Is there a way to NOT have this logged?
            self.queue_game.put(
                "TWGS Proxy build "
                + version
                + " 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.game = game
                log.msg("Game: {0}".format(self.game))
                self.game = game
            #     self.setGame(game)

    def dataReceived(self, chunk):
        """ Data received from the Game. 
        
        Remove backspaces.
        Treat some ANSI codes as NewLine.        
        Remove ANSI.
        Break into lines.
        Trim out carriage returns.
        Call lineReceived().
        
        "Optionally" pass data to player.
        FUTURE: trigger on prompt. [cleanANSI(buffer)]
        """

        # Sequence error:
        # If I don't put the chunk(I received) to the player.
        # anything I display -- lineReceive() put() ... would
        # be out of order.  (I'd be responding -- before it
        # was displayed to the user.)

        # Possibly:  if self.passon ... send to player
        self.queue_game.put(chunk)

        self.buffer += chunk.decode("utf-8", "ignore")

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

        # Treat some ANSI codes as a newline
        self.buffer = treatAsNL(self.buffer)

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

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


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

    def __init__(self, player):
        self.player = player
        self.queue_player = player.queue_player
        self.queue_game = player.queue_game
        self.observer = player.observer

    def closeIt(self):
        log.msg("closeIt")
        self.queue_game.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_game.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.user = None
        self.observer = Observer()

    def connectionMade(self):
        """ connected, setup queues. 
        
        queue_player is data from player.
        queue_game is data to player. (possibly from game)
        """
        self.queue_player = defer.DeferredQueue()
        self.queue_game = defer.DeferredQueue()
        self.setGameReceived()

        # Connect GlueFactory to this Player object.
        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 gameDataReceived(self, chunk):
        """ Data received from the client/player. """
        if chunk is False:
            self.transport.loseConnection()
        else:
            if type(chunk) == bytes:
                self.transport.write(chunk)
            elif type(chunk) == str:
                self.transport.write(chunk.encode())
            else:
                log.err("gameDataReceived: type (%s) given!", type(chunk))
                self.transport.write(chunk)
            self.setGameReceived()

    def dataReceived(self, chunk):
        if self.user is None:
            self.buffer += chunk.decode("utf-8", "ignore")

            parts = self.buffer.split("\x00")
            if len(parts) >= 5:
                # rlogin we have the username
                self.user = parts[1]
                log.msg("User: {0}".format(self.user))
                zpos = self.buffer.rindex("\x00")
                self.buffer = self.buffer[zpos + 1 :]
                # but I don't need the buffer anymore, so:
                self.buffer = ""
                # Pass user value on to whatever needs it.

        self.queue_player.put(chunk)

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

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


if __name__ == "__main__":
    if LOGFILE:
        log.startLogging(DailyLogFile("proxy.log", "."))
    else:
        log.startLogging(sys.stdout)

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