import time
import tempfile
import random
import traceback
import thread

import trac.core # for interface and extension points

from twisted.spread import pb
from twisted.internet import reactor

import config
from interfaces import IAssignment
import assignments

# exceptions

class BattleError(pb.Error):
    """used to communicate problems from the server to the client"""

class RemoteException(Exception):
    """raised on the client to wrap a remote exception"""

# report related stuff

class Error(object):
    def __init__(self, result, exception, e, tb):
        self.result = result
        self.exception = exception
        self.message = e
        self.traceback = traceback.format_tb(tb)

class Parted(object):
    def __init__(self):
        self.result = None

class Invalid(object):
    def __init__(self, result, message):
        self.result = result
        self.message = message

class Success(object):
    def __init__(self, result, code_size, time_spent, execution_time):
        self.result = result
        self.code_size = code_size
        self.time_spent = time_spent
        self.execution_time = execution_time

class Report(pb.Referenceable):
    """a report"""

    def __init__(self):
        self.results = {}

    def report_part(self, username):
        self.results[username] = Parted()

    def report_error(self, username, result, exception, e, tb):
        self.results[username] = Error(result, exception, e, tb)

    def report_invalid(self, username, result, error):
        self.results[username] = Invalid(result, error)

    def report_success(self, username, result, code_size, time_spent,
                        execution_time):
        self.results[username] = Success(result, code_size, time_spent,
                                            execution_time)

    def __str__(self):
        ret = ['results:']
        for username, result in self.results.iteritems():
            ret.append('user %s' % (username,))
            ret.append('')
            ret.append('submitted code:')
            ret.append(str(result.result))
            ret.append('')
            if isinstance(result, Error):
                ret.append('Error: %s - %s' % 
                            (result.exception, result.message))
                ret.append('')
                ret.append('\n'.join(result.traceback))
            elif isinstance(result, Invalid):
                ret.append('Invalid: %s' % (result.message))
            elif isinstance(result, Success):
                ret.append('Success...')
                ret.append('Code size: %s instructions' % (result.code_size,))
                ret.append('Time spent: %s seconds' % (result.time_spent,))
                ret.append('Execution time of loops: %s' % 
                            (result.execution_time,))
            ret.append('')
        return '\n'.join(ret)

    def remote_stringify(self):
        return str(self)

# server related stuff

class States:
    INITIALIZING = -1
    ACCEPTING = 0
    PLAYING = 1
    REPORTING = 2

def log(m, severity=0):
    """log a message

        m is the message, severity can be 0 (informative), 1 (warning) or 2
        (error)
    """
    print 'log %s: %s' % (time.strftime('%Y/%m/%d %H:%M:%S'), m)

class PyBattleServer(trac.core.Component, pb.Root):
    """simple game server"""
    
    # automagically find all IAssignment classes and make a 'list' of
    # instances available
    assignments = trac.core.ExtensionPoint(IAssignment)
    status = States.INITIALIZING

    def __init__(self):
        self.clients = {}
        self._initialize()
        log('server initialized')
        
    # remote API
        
    def remote_connect(self, username, client):
        """try to register for the next game"""
        if username in self.clients:
            raise BattleError('name already in use')
        self.clients[username] = client
        client.server_data = {'result': None,
                                'joined': False,
                                'parted': False,
                                }
        log('user %s connected' % (username,))
        self.chat('pybattle', 'user %s connected' % (username,))
        self.chat('pybattle', 
                'current users now: %s' % (', '.join(self.clients.keys())))

    def remote_join(self, username):
        if self.status != States.ACCEPTING:
            raise BattleError('joining currently not allowed')
        if username not in self.clients:
            raise BattleError('unkown username')
        data = self.clients[username].server_data
        if data['parted']:
            raise BattleError('already parted')
        if data['joined']:
            raise BattleError('already joined')
        data['joined'] = True
        log('user %s joined the game' % (username,))
        self.chat('pybattle', 'user %s joined' % (username,))

    def remote_start(self, *args):
        """start this round"""
        if not self.status == States.ACCEPTING:
            raise BattleError('not ready to start now...')
        log('starting game')
        players = sorted(
                    [u for u, c in self.clients.iteritems() if 
                        c.server_data['joined'] and not 
                        c.server_data['parted']
                    ])
        self.chat('pybattle', 
                'starting game with players: %s' % (', '.join(players)))
        self.status = States.PLAYING
        # choose a random assignment and 
        self.assignment = assignment = random.choice(list(self.assignments))
        self.assignment.start_time = time.time()
        question = assignment.question
        log('assignment: %s' % (question,))
        for username, client in self.clients.iteritems():
            if client.server_data['parted']:
                continue
            if not client.server_data['joined']:
                client.callRemote('print_question', question)
                continue
            log('calling start_assignment for user %s' % (username,))
            client.callRemote('start_assignment', question)

    def remote_stop(self, username, result):
        """save result for user"""
        if not username in self.clients:
            print 'stop'
            raise BattleError('unknown user %s' % (username,))
        data = self.clients[username].server_data
        if data['parted'] or not data['joined']:
            raise BattleError('not joined')
        log('results for user %s: %s' % (username, result))
        self.chat('pybattle', 'results for user %s are in' % (username,))
        data['result'] = result
        data['time_spent'] = time.time() - self.assignment.start_time
        # see if all results are in now, if so procss and present them
        for username, client in self.clients.iteritems():
            if not client.server_data['joined']:
                continue
            if (not client.server_data['parted'] and 
                    client.server_data['result'] is None):
                return
        self._process_and_present()

    def remote_part(self, username):
        if not username in self.clients:
            print 'part'
            raise BattleError('unknown user %s' % (username,))
        data = self.clients[username].server_data
        if data['parted'] or not data['joined']:
            raise BattleError('not joined')
        data['joined'] = False
        data['parted'] = True
        log('%s parted' % (username,))
        self.chat('pybattle', '%s parted' % (username,))
        # if there are no clients left, stop the game and restart
        for username, client in self.clients.iteritems():
            print username, repr(client.server_data)
            if (not client.server_data['parted'] and
                    client.server_data['result'] is None):
                return
        self._process_and_present()

    def remote_disconnect(self, username):
        if not username in self.clients:
            print 'disconnect'
            raise BattleError('unknown user %s' % (username,))
        del self.clients[username]
        print 'disconnected'
        self.chat('pybattle', '%s disconnected' % (username,))
        if self.status == States.PLAYING:
            for username, client in self.clients.iteritems():
                if (not client.server_data['parted'] and
                        client.server_data_result is None):
                    return
            self._process_and_present()

    def remote_chat(self, username, message):
        if (not username in self.clients or 
                self.clients[username].server_data['parted']):
            raise BattleError('no such user %s' % (username,))
        self.chat(username, message)

    # public/own API

    def chat(self, user, message):
        log('chat: %s said %s' % (user, message))
        for username, client in self.clients.iteritems():
            d = client.callRemote('chat', user, message)

    # private stuff

    def _initialize(self):
        log('initializing')
        if self.status != States.INITIALIZING:
            raise Exception, 'can not initialize now'
        for username, client in self.clients.iteritems():
            client.server_data = {
                'result': None,
                'joined': False,
                'parted': False,
            }
        self.status = States.ACCEPTING
        log('accepting')

    def _cleanup(self):
        log('cleanup')
        self.status = States.INITIALIZING

    def _process_and_present(self):
        """process the results and present the report to clients"""
        self.status = States.REPORTING
        report = self._process_results()
        for username, client in self.clients.iteritems():
            if client.server_data['parted']:
                continue
            log('presenting report to %s' % (username,))
            client.callRemote('present_report', report)
        # we're done, can start accepting new registrations again
        self._cleanup()
        self._initialize()

    def _process_results(self):
        """create a Report instance, fill it with data about the results"""
        report = Report()
        for username, client in self.clients.iteritems():
            result = client.server_data['result']
            self.assignment.test(username, client, result, report)
        return report

# the client

class PyBattleClient(pb.Root):
    """game client interface"""

    connected = False
    joined = False
    playing = False
    result = None
    
    def __init__(self, username):
        self.username = username

    # PUBLIC API

    def _init(self, server):
        self.server = server

    def connect(self):
        """connect to the server"""
        if self.connected:
            raise BattleError('already connected')
        d = self.server.callRemote('connect', self.username, self)
        d.addCallbacks(self._connected, self._error)

    def join(self):
        """join the (if any) upcoming game"""
        if not self.connected:
            raise BattleError('not connected yet')
        if self.joined:
            raise BattleError('already joined')
        d = self.server.callRemote('join', self.username)
        d.addCallbacks(self._joined, self._error)
    
    def start(self):
        """start the game *for all clients!*"""
        if not self.connected:
            raise BattleError('not connected yet!')
        if not self.joined:
            raise BattleError(
                    'you have to join the game in order to start it')
        if self.playing:
            raise BattleError('already playing!')
        # just to be sure ;)
        self.result = None
        d = self.server.callRemote('start', self)
        d.addErrback(self._error)

    def stop(self):
        """stop and save the results"""
        if not self.connected:
            raise BattleError('not connected yet')
        if not self.joined:
            raise BattleError('not joined yet')
        if not self.playing:
            raise BattleError('not playing yet')
        d = self.server.callRemote('stop', self.username, self.result)
        d.addErrback(self._error)
        self._reset()

    def part(self):
        """stop and discard the results (forfeit)"""
        if not self.connected or not self.joined:
            raise BattleError('not joined yet!')
        if not self.playing:
            raise BattleError('not playing yet!')
        d = self.server.callRemote('part', self.username)
        d.addErrback(self._error)
        self._reset()

    def disconnect(self):
        """disconnect from the server"""
        if not self.connected:
            raise BattleError('not connected!')
        self._reset()
        d = self.server.callRemote('disconnect', self.username)
        d.addCallbacks(self._quit, self._error)
        self.connected = False
    
    def set_result(self, result):
        self.result = result

    def chat(self, message):
        self.server.callRemote('chat', self.username, message)

    def show_info(self, info):
        """show info message"""
        self._print(info)

    # REMOTE API (called by server)

    def remote_print_question(self, question):
        """print the question, for when not joined"""
        self.show_info('assignment: %s' % question)

    def remote_start_assignment(self, question):
        """start editing"""
        self.show_info('assignment: %s' % (question,))
        self.result = None
        self.playing = True

    def remote_present_report(self, report):
        """present the report"""
        # XXX a bit strange to have this here... seems to be correct though?
        self.joined = False
        def print_report(report):
            self._print(report)
        report.callRemote('stringify').addCallbacks(print_report, self._error)

    def remote_chat(self, username, message):
        """present chat message"""
        self._print('%s said: %s' % (username, message))

    def _connected(self, ret=None):
        self.connected = True

    def _joined(self, ret=None):
        self.joined = True

    def _quit(self, ret=None):
        if self.connected:
            self.disconnect()
        else:
            reactor.stop()

    def _print(self, s):
        print s

    def _error(self, err):
        if type(err) not in [str, unicode]:
            if not isinstance(err, pb.Error):
                raise RemoteException(err)
            err = err.getErrorMessage()
        self._print('err: %s' % err)

    def _reset(self):
        self.playing = False
        self.joined = False
        self.result = False

# functions to start the server and (a quite useless implementation of the)
# client

def start_server():
    warning = ['You are about to run a *very* insecure program!',
                '',
                'This server executes arbitrary Python code presented by ',
                'arbitrary clients in an unrestricted manner. This makes it',
                'a very insecure system, that can easily be used for',
                'malicious purposes. Make *very* sure the user running the',
                'server doesn\'t have any privileges whatsoever on the',
                'system the server runs on, preferrably behind a firewall,',
                'and play only with people you trust will not do anything',
                'unsportsmenlike (unless your rules are to destroy the server',
                'as efficiently as possible, or something ;).',
                '',
                'Enter \'I am sure, start the server!\' to continue, any',
                'other input to quit.',
                '']
    if raw_input('\n'.join(warning)) != 'I am sure, start the server!':
        return
    component_manager = trac.core.ComponentManager()
    root = PyBattleServer(component_manager)
    log('starting server with assignments %s' % ([a.__class__.__name__ for 
                                                    a in root.assignments],))
    reactor.listenTCP(config.server_port, pb.PBServerFactory(root))
    reactor.run()

def start_client(username):
    client = PyBattleClient(username)
    factory = pb.PBClientFactory()
    reactor.connectTCP(config.server_hostname, config.server_port, factory)
    d = factory.getRootObject()
    d.addCallbacks(client._init, client._error)
    reactor.run()

