# -*- coding: utf-8 -*-
import os
import protocol
import xml.sax.saxutils
import gettext

_ = gettext.gettext

class Channel:
    """
    """    
        
    def logXML(self, text):
        pass
        
    def logBinary(self, data):
        pass
    
    def connect(self, host, port):
        pass
    
    def send(self, data, isBinary = False):
        pass
    
    def close(self):
        pass
    
class ChannelFeedback:
    """
    """
    
    def registerIncomingData(self, data):
        pass

    def onSessionEnded(self):
        pass
    
    def closed(self, errno = 0):
        pass
    
class ClientFeedback:
    
    def setBusy(self, isBusy):
        """Called when the client is busy (unable to take requests)"""
        pass
    
    def onConnected(self):
        pass

    def onDisconnected(self, reason):
        pass
    
    def openChannel(self, feedback):
        """Open a channel to the GGZ server.
        
        'feedback' is the object to notify data received on this channel.
        """
        pass
    
    def getLogin(self):
        """Called when the login credentials are required.
        
        Returns the username and password to log in with.
        If the password is None then log in as guest.
        """
        return ('test', None)
    
    def getPassword(self, username):
        """Called when a password is required.
        
        'username' is the username the password is required for.
        """
        return None
    
    def onMOTD(self, motd):
        """Called when the message of the day is received.
        
        'motd' is the message received.
        """
        pass
    
    def roomAdded(self, room):
        """Called when a room is added.
        
        'room' the new room.
        """
        pass
    
    def roomUpdated(self, room):
        pass

    def roomJoined(self, room):
        pass
    

    def tableAdded(self, table):
        pass

    def tableUpdated(self, table):
        pass

    def tableRemoved(self, table):
        pass
    

    def playerAdded(self, player):
        pass

    def playerRemoved(self, player):
        pass
    
    
    def onChat(self, chatType, sender, text):
        pass

class Game:
    
    def __init__(self):
        self.id = ''
        self.name = ''
        self.version = ''
        self.protocol_engine = ''
        self.protocol_version = ''
        self.nPlayers = 0
        self.author = ''
        self.url = ''

class Room:
    
    def __init__(self):
        self.id = ''
        self.game = None    
        self.nPlayers = 0

class Table:
    
    def __init__(self, nSeats):
        self.id = ''
        self.room = ''
        self.game = None
        self.status = ''
        self.description = ''
        self.seats = []
        for i in xrange(nSeats):
            seat = Seat()
            self.seats.append(seat)

class Seat:
    
    def __init__(self):
        self.type = ''
        self.user = ''

class Player:
    
    def __init__(self):
        self.name = ''
        self.type = ''
        self.table = None
        self.perms = 0
        self.lag = 0
        self.room = None
        self.lastRoom = None
    
class MainChannel(ChannelFeedback, protocol.ParserFeedback):

    def __init__(self, client):
        self.client = client
        self.decoder = protocol.Decoder(self)

    def registerIncomingData(self, data):
        self.controller.logXML(data)
        while len(data) > 0:
            data = self.decoder.feed(data)

    def send(self, data, isBinary = False):
        self.controller.send(data, isBinary)

    def onConnected(self):
        assert(self.client.state is self.client.STATE_DISCONNECTED)
        self.client.feedback.onConnected()
        self.client.setState(self.client.STATE_START_SESSION)
        
        try:
            language = os.environ['LANG']
        except KeyError:
            language = 'C'

        self.send("<?xml version='1.0' encoding='UTF-8'?>\n")
        self.send("<SESSION>\n")
        self.send("<LANGUAGE>%s</LANGUAGE>\n" % xml.sax.saxutils.escape(language))
        (self.client.username, self.client.password) = self.client.feedback.getLogin()
        if self.client.password is None:
            self.client._loginGuest(self.client.username)
        else:
            self.client._login(self.client.username, self.client.password)
        
    def onResult(self, action, code):
        if action == 'login':
            if code == 'ok':
                self.client.setState(self.client.STATE_LIST_GAMES)
            else:
                if code == 'usr lookup':
                    # Translators: GGZ disconnection error when the supplied password is incorrect
                    self.client.close(_('Incorrect password'))
                    #FIXME: Prompt for a password
                    #self.client.setState(self.client.STATE_GET_PASSWORD)

                elif code == 'already logged in':
                    # Translators: GGZ disconnection error when the selected account is already in use
                    self.client.close(_('Account in use'))
                    #FIXME: If guest then prompt for a new user or mangle username until one can be found
                    
                elif code == 'wrong login type':
                    self.client.setState(self.client.STATE_GET_PASSWORD)
                
                else:
                    self.client.close(code)

        elif action == 'enter':
            if code != 'ok':
                print 'Failed to enter room'
                self.client.setState(self.client.STATE_READY)
                return
            
            if self.client.room is not None:
                self.client.room.nPlayers -= 1
                self.client.feedback.roomUpdated(self.client.room)
                
            room = self.client.enteringRoom

            self.client.room = room
            self.client.players = {}
            self.client.tables = {}
            room.nPlayers = 0
            self.client.setState(self.client.STATE_LIST_TABLES)

        elif action == 'list':
            if self.client.state is self.client.STATE_LIST_GAMES:
                self.client.setState(self.client.STATE_LIST_ROOMS)
                
            elif self.client.state is self.client.STATE_LIST_ROOMS:
                for room in self.client.rooms.itervalues():
                    if room.game is None:
                        break
                # FIXME: Check have valid room, otherwise go to room 0
                
                self.client.setState(self.client.STATE_ENTERING_ROOM)
                self.client.enteringRoom = room
                self.send("<ENTER ROOM='%s'/>\n" % room.id)
                
            elif self.client.state is self.client.STATE_LIST_TABLES:
                self.client.setState(self.client.STATE_LIST_PLAYERS)
                
            elif self.client.state is self.client.STATE_LIST_PLAYERS:
                self.client.setState(self.client.STATE_READY)
                self.client.feedback.roomEntered(self.client.enteringRoom)

        elif action == 'chat':
            pass# FIXME: could be AT_TABLE self.client.setState(self.client.STATE_READY)

        elif action == 'launch':
            self.client.setState(self.client.STATE_READY)
            
        elif action == 'join':
            if code == 'ok':
                self.client.setState(self.client.STATE_AT_TABLE)
            elif code == 'table full':
                print 'Failed to join table: table full'
                self.client.setState(self.client.STATE_READY)
            else:
                print 'Unknown join result: %s' % action
                
        elif action == 'leave':
            if code == 'ok':
                self.client.setState(self.client.STATE_READY)
                pass # TODO
            else:
                print 'Unknown leave result: %s' % action

        else:
            print 'Unknown result: %s %s' % (action, code)

    def onMOTD(self, motd):
        self.client.feedback.onMOTD(motd)

    def onChat(self, chatType, sender, text):
        self.client.feedback.onChat(chatType, sender, text)
        
    def onJoin(self, tableId, isSpectator):
        try:
            table = self._getTable(tableId)
        except KeyError:
            print "Unknown JOIN with TABLE='%s'" % tableId
            return
        self.client.setState(self.client.STATE_AT_TABLE)
        g = self.client.feedback.onJoin(table, isSpectator, self.client.channel)
        self.client.channel.setGame(g)
        
    def onLeave(self, reason):
        self.client.feedback.onLeave(reason)
        
    def gameAdded(self, gameId, name, version, author, url, numPlayers, protocol_engine, protocol_version):
        game = Game()
        game.id = gameId
        game.name = name
        game.version = version
        game.author = author
        game.url = url
        game.numPlayers = numPlayers # FIXME: Make min/max (e.g. can be '1..3')
        game.protocol_engine = protocol_engine
        game.protocol_version = protocol_version
        self.client.games[gameId] = game

    def roomAdded(self, roomId, gameId, name, description, nPlayers):
        try:
            game = self._getGame(gameId, optional = True)
        except KeyError:
            print "Unknown ROOM ADD with GAME='%s'" % gameId
            return
        room = Room()
        room.id = roomId
        room.game = game
        room.name = name
        room.description = description
        room.nPlayers = int(nPlayers)
        self.client.rooms[roomId] = room
        self.client.feedback.roomAdded(room)

    def roomPlayersUpdate(self, roomId, nPlayers):
        try:
            room = self._getRoom(roomId)
        except KeyError:
            print "Unknown ROOM PLAYER UPDATE with ROOM='%s'" % roomId
            return
        room.nPlayers = int(nPlayers)
        self.client.feedback.roomUpdated(room)

    def tableAdded(self, roomId, tableId, gameId, status, nSeats, description): 
        try:
            room = self._getRoom(roomId)
            game = self._getGame(gameId)
        except KeyError:
            print "Unknown TABLE ADD with ROOM='%s' GAME='%s'" % (roomId, gameId)
            return
        table = Table(int(nSeats))
        table.id = tableId
        table.room = room
        table.game = game
        table.status = status
        table.description = description
        self.client.tables[tableId] = table
        self.client.feedback.tableAdded(table)

    def tableStatusChanged(self, tableId, status):
        try:
            table = self._getTable(tableId)
        except KeyError:
            print "Unknown TABLE STATUS with TABLE='%s'" % tableId
            return
        table.status = status
        self.client.feedback.tableUpdated(table)

    def seatChanged(self, roomId, tableId, seatId, seatType, user):
        try:
            table = self._getTable(tableId)
        except KeyError:
            print "Unknown SEAT CHANGE with TABLE='%s'" % tableId
            return
        if table.room.id != roomId:
            return
        seat = table.seats[int(seatId)]
        seat.type = seatType
        seat.user = user
        self.client.feedback.tableUpdated(table)

    def tableRemoved(self, tableId):
        try:
            table = self.client.tables.pop(tableId)
        except KeyError: 
            print "Unknown TABLE REMOVE with TABLE='%s'" % tableId           
            # We do not know of this table - this could occur if we receive a
            # table remove event before we get the table list.
            return
        self.client.feedback.tableRemoved(table)
        
    def onPlayerList(self, roomId, players):
        try:
            room = self._getRoom(roomId)
            for p in players:
                _ = self._getTable(p.tableId, optional = True)
        except KeyError: 
            print "Unknown PLAYER LIST with ROOM='%s'" % roomId
            return
        self.client.players = {}
        for p in players:
            player = Player()
            player.name = p.name
            player.type = p.type
            player.table = self._getTable(p.tableId, optional = True)
            player.perms = p.perms
            player.lag = p.lag
            player.room = room
            self.client.players[player.name] = player

        room.nPlayers = len(players)
        self.client.feedback.roomUpdated(room)

    def playerAdded(self, name, playerType, tableId, perms, lag, roomId, fromRoomId):
        try:
            room = self._getRoom(roomId, optional = True)
            lastRoom = self.client.rooms[fromRoomId]
            table = self._getTable(tableId, optional = True)
        except KeyError:
            print "Unknown PLAYER ADD with ROOM='%s' LASTROOM='%s' TABLE='%s'" % (roomId, fromRoomId, tableId)
            return
        player = Player()
        player.name = name
        player.type = playerType
        player.table = table
        player.perms = perms
        player.lag = lag
        player.room = room
        player.lastRoom = lastRoom
        self.client.players[player.name] = player

        if player.lastRoom is not None:
            player.lastRoom.nPlayers -= 1
            self.client.feedback.roomUpdated(player.lastRoom)
        if player.room is not None:
            player.room.nPlayers += 1
            self.client.feedback.roomUpdated(player.room)
        self.client.feedback.playerAdded(player)

    def playerRemoved(self, name, roomId, toRoomId):
        try:
            player = self.client.players.pop(name)
            room = self._getRoom(toRoomId, optional = True)
        except KeyError:
            print "Unknown PLAYER REMOVE with NAME='%s' ROOM='%s' TOROOM='%s'" % (name, roomId, toRoomId)
            # We do not know of this player - this could occur if we receive a
            # player remove event before we get the player list.
            return
            
        player.lastRoom = player.room
        player.room = room
        if player.room is not None:
            player.room.nPlayers += 1
            self.client.feedback.roomUpdated(player.room)
        if player.lastRoom is not None:
            player.lastRoom.nPlayers -= 1
            self.client.feedback.roomUpdated(player.lastRoom)
        self.client.feedback.playerRemoved(player)

    def closed(self, errno = 0):
        # Translators: GGZ disconnection error when the network link has broken. %s is the system provided error
        self.client.close(_('Connection closed: %s') % os.strerror(errno))
       
    def _getGame(self, gameId, optional = False):
        if optional and gameId == '-1':
            return None
        return self.client.games[gameId]

    def _getRoom(self, roomId, optional = False):
        if optional and roomId == '-1':
            return None
        return self.client.rooms[roomId]    
        
    def _getTable(self, tableId, optional = False):
        if optional and tableId == '-1':
            return None
        return self.client.tables[tableId]
    
class GameChannel(ChannelFeedback, protocol.ParserFeedback):
    
    def __init__(self, client, command):
        self.client = client
        self.command = command
        self.inSession = True
        self.decoder = protocol.Decoder(self)
        self.buffer = ''
        self.game = None
        
    def setGame(self, game):
        self.game = game
        if len(self.buffer) > 0:
            self.game.registerIncomingData(self.buffer)
        self.buffer = ''

    def registerIncomingData(self, data):
        while self.inSession and len(data) > 0:
            remainder = self.decoder.feed(data)
            self.controller.logXML(data[:len(data) - len(remainder)])
            data = remainder

        if len(data) == 0:
            return
        
        self.controller.logBinary(data)

        if self.game is None:
            self.buffer += data
        else:
            self.game.registerIncomingData(data)

    def onDisconnected(self, reason):
        print 'Disconnected: %s' % reason

    def send(self, data, isBinary = False):
        self.controller.send(data, isBinary)

    def onConnected(self):
        self.send("<?xml version='1.0' encoding='UTF-8'?>\n")
        self.send("<SESSION>\n")
        self.send("<LANGUAGE>en_NZ.UTF-8</LANGUAGE>\n")
        self.send("<CHANNEL ID='%s' /></SESSION>\n" % self.client.username)
        
    def onSessionEnded(self):
        self.inSession = False
        self.client.mainChannel.send(self.command)

    def closed(self, errno = 0):
        print 'SEVERE: GGZ channel closed'

class Client:
    
    STATE_DISCONNECTED        = 'DISCONNECTED'
    STATE_START_SESSION       = 'START_SESSION'
    STATE_LOGIN               = 'LOGIN'
    STATE_GET_PASSWORD        = 'GET_PASSWORD'    
    STATE_LIST_GAMES          = 'LIST_GAMES'
    STATE_LIST_ROOMS          = 'LIST_ROOMS'
    STATE_READY               = 'READY'
    STATE_JOIN_TABLE_CHANNEL  = 'JOIN_TABLE_CHANNEL'
    STATE_JOIN_TABLE          = 'JOIN_TABLE'
    STATE_START_TABLE_CHANNEL = 'START_TABLE_CHANNEL'
    STATE_START_TABLE         = 'START_TABLE'
    STATE_AT_TABLE            = 'AT_TABLE'
    STATE_LEAVE_TABLE         = 'LEAVE_TABLE'
    STATE_ENTERING_ROOM       = 'ENTERING_ROOM'
    STATE_LIST_TABLES         = 'LIST_TABLES'
    STATE_LIST_PLAYERS        = 'LIST_PLAYERS'

    def __init__(self, feedback):
        self.feedback = feedback
        self.commands = []
        self.sending = False
        self.games = {}
        self.rooms = {}
        self.tables = {}
        self.players = {}
        self.room = None
        self.state = self.STATE_DISCONNECTED
        
    def isReady(self):
        """Check if ready for new requests.
        
        Returns True if can make a new request.
        """
        return self.state is self.STATE_READY

    def close(self, error):
        self.disconnectionError = error
        self.setState(self.STATE_DISCONNECTED)
        
    def isBusy(self):
        return not (self.state is self.STATE_READY or self.state is self.STATE_AT_TABLE or self.state is self.STATE_DISCONNECTED)

    def setState(self, state):
        print 'Changing state from %s to %s' % (self.state, state)
        self.state = state

        self.feedback.setBusy(self.isBusy())
        
        if state is self.STATE_LIST_GAMES:
            self.mainChannel.send("<LIST TYPE='game' FULL='true'/>\n")
        elif state is self.STATE_LIST_ROOMS:
            self.mainChannel.send("<LIST TYPE='room' FULL='true'/>\n")
        elif state is self.STATE_LIST_TABLES:
            self.mainChannel.send("<LIST TYPE='table'/>\n")
        elif state is self.STATE_LIST_PLAYERS:
            self.mainChannel.send("<LIST TYPE='player'/>\n")
        elif state is self.STATE_GET_PASSWORD:
            self.feedback.getPassword(self.username)
        elif state is self.STATE_DISCONNECTED:
            self.feedback.onDisconnected(self.disconnectionError)
            self.mainChannel.controller.close()

    def start(self):
        assert(self.state is self.STATE_DISCONNECTED)
        self.mainChannel = MainChannel(self)
        self.mainChannel.controller = self.feedback.openChannel(self.mainChannel)
        
    def setPassword(self, password):
        assert(self.state is self.STATE_GET_PASSWORD)
        if password is None:
            # Translators: GGZ disconnection error when a password was required for the selected account
            self.close(_('A password is required'))
        else:
            self._login(self.username, password)
        
    def _login(self, username, password):
        assert(self.state is self.STATE_START_SESSION or self.state is self.STATE_GET_PASSWORD)
        self.setState(self.STATE_LOGIN)
        username = xml.sax.saxutils.escape(username)
        password = xml.sax.saxutils.escape(password)
        self.mainChannel.send("<LOGIN TYPE='normal'><NAME>%s</NAME><PASSWORD>%s</PASSWORD></LOGIN>\n" % (username, password));
        
    def _loginGuest(self, username):
        assert(self.state is self.STATE_START_SESSION)
        self.setState(self.STATE_LOGIN)
        username = xml.sax.saxutils.escape(username)
        self.mainChannel.send("<LOGIN TYPE='guest'><NAME>%s</NAME></LOGIN>\n" % username);
        
    def _loginNew(self, username, password, email):
        assert(self.state is self.STATE_START_SESSION)
        self.setState(self.STATE_LOGIN)
        username = xml.sax.saxutils.escape(username)
        password = xml.sax.saxutils.escape(password)
        email = xml.sax.saxutils.escape(email)
        self.mainChannel.send("<LOGIN TYPE='first'><NAME>%s</NAME><PASSWORD>%s</PASSWORD><EMAIL>%s</EMAIL></LOGIN>\n" % (username, password, email));
        
    def enterRoom(self, room):
        if self.state is self.STATE_AT_TABLE:
            print 'At table'
            return
        else:
            assert(self.state is self.STATE_READY)
        self.setState(self.STATE_ENTERING_ROOM)
        self.enteringRoom = room
        self.mainChannel.send("<ENTER ROOM='%s'/>\n" % room.id)
        
    def startTable(self, gameId, description, player):
        if self.state is not self.STATE_READY:
            print 'Unable to start table'
            return
        self.setState(self.STATE_START_TABLE)
        
        # Seat types are 'open', 'bot' or 'reserved' or 'player' (latter two have player name in them)
        command = "<LAUNCH>\n"
        command += "<TABLE GAME='%s' SEATS='2'>\n" % gameId
        command += "<DESC>%s</DESC>\n" % xml.sax.saxutils.escape(description)
        command += "<SEAT NUM='0' TYPE='reserved'>%s</SEAT>\n" % player
        command += "<SEAT NUM='1' TYPE='open'/>\n"
        command += "</TABLE>\n"
        command += "</LAUNCH>\n"
        
        self.channel = GameChannel(self, command)
        self.channel.controller = self.feedback.openChannel(self.channel)
        
    def joinTable(self, table):
        if self.state is self.STATE_AT_TABLE:
            print 'Already at table'
            return
        
        assert(self.state is self.STATE_READY)
        self.setState(self.STATE_JOIN_TABLE)
        
        command = "<JOIN TABLE='%s' SPECTATOR='false'/>\n" % table.id
        self.channel = GameChannel(self, command)
        self.channel.controller = self.feedback.openChannel(self.channel)

    def leaveTable(self):
        assert(self.state is self.STATE_AT_TABLE)
        self.setState(self.STATE_LEAVE_TABLE)
        self.mainChannel.send("<LEAVE FORCE='true'/>\n")
        
    def sendChat(self, text):
        self.mainChannel.send("<CHAT TYPE='normal'>%s</CHAT>\n" % xml.sax.saxutils.escape(text))
