# TOFU
# Copyright (C) 2005 Jean-Baptiste LAMY
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

"""tofu.client

This module implements a Tofu client.

To lauch a server, create a GameInterface instance, import this module,
and then call serve_forever().
"""

from twisted.internet.protocol import DatagramProtocol, Protocol, Factory, ClientFactory
from twisted.protocols.basic import LineReceiver, NetstringReceiver
from twisted.python.failure import Failure
#from twisted.internet import reactor
import twisted.internet.selectreactor
#reactor = twisted.internet.selectreactor.SelectReactor()

import sys, struct
import tofu

try: set
except: from sets import Set as set

class ClientServerError(StandardError): pass


class UDP(DatagramProtocol):
  def startProtocol(self):
    a = "hello"
    self.transport.write(a, ("127.0.0.1", 9999))
    
  def datagramReceived(self, data, (host, port)):
    print "UDP received %r from %s:%d" % (data, host, port)
    
PLANNED_ARRIVAL_UIDS = set()

WAITERS = {}
class Waiter(object):
  def __init__(self, callback = lambda *args: None):
    self.callback         = callback
    self.uniques          = set([])
    self.nb_waited_unique = 0
    
  def wait_for(self, uid, ask_for_it = 1):
    if tofu.Unique.hasuid(uid): # Already available
      self.uniques.add(tofu.Unique.getbyuid(uid))
    else:
      #if ask_for_it:
      #  if not uid in tofu.NOTIFIER.uids_arrival_planned:
      #    tofu.NOTIFIER.ask_unique(uid)
      
      self.nb_waited_unique += 1
      waiters = WAITERS.get(uid)
      if not waiters: waiters = WAITERS[uid] = []
      waiters.append(self)
      
  def arrived(self, unique):
    self.uniques.add(unique)
    self.nb_waited_unique -= 1
    if self.nb_waited_unique == 0: self.callback(*self.uniques)
    
  def start(self):
    if self.nb_waited_unique == 0: self.callback(*self.uniques)
    
    
MAX_LENGTH = 99999

class Notifier(NetstringReceiver, tofu.Notifier):
  def __init__(self):
    tofu.Notifier.__init__(self)
    
    self.errors = []
    self.MAX_LENGTH           = MAX_LENGTH
    self.uids_arrival_planned = set()
    
  def connectionMade(self):
    self.sendString(tofu.CODE_CHECK_VERSION + tofu.VERSION)
    tofu.GAME_INTERFACE.ready(self)
    
  def login_player(self, filename, password, client_side_data = ""):
    self.sendString("%s%s\n%s\n%s" % (tofu.CODE_LOGIN_PLAYER, filename, password, client_side_data))
    
  def logout_player(self):
    self.sendString(tofu.CODE_LOGOUT_PLAYER)
    
  def stringReceived(self, data):
    #print "TCP receive:", repr(data)
    #print len(data)
    
    code = data[0]
    #print "* Tofu * receiving code %s..." % code
    if   code == tofu.CODE_DATA_STATE:
      state = tofu.YourState.undump(data[5:])
      uid   = struct.unpack("!i", data[1:5])[0]
      if tofu.Unique.hasuid(uid): tofu.Unique.getbyuid(uid).doer.push(state)
      elif state.is_crucial():
        pass
      #  XXX
      #  waiter = Waiter(lambda mobile: mobile.doer.push(state))
      #  waiter.wait_for(uid)
      #  waiter.start()
      
    elif code == tofu.CODE_OWN_CONTROL:
      uid = struct.unpack("!i", data[1:])[0]
      print "* Tofu * Owning mobile %s..." % uid
      #def own_control(mobile):
      #  print "own_control", mobile.level
      #  tofu.IDLER.next_round_tasks.append(mobile.control_owned)
      #waiter = Waiter(own_control)
      
      waiter = Waiter(lambda mobile: mobile.control_owned())
      
      waiter.wait_for(uid, 0)
      waiter.start()
      
    elif code == tofu.CODE_REMOVE_MOBILE:
      uid = struct.unpack("!i", data[1:])[0]
      print "* Tofu * Removing mobile %s..." % uid
      def remove_mobile(mobile):
        mobile.level.remove_mobile(mobile)
        mobile.discard()
      #def remove_mobile(mobile):
      #  def remove_mobile2():
      #    print "* Tofu * Mobile %s removed !" % mobile.uid
      #    mobile.level.remove_mobile(mobile)
      #    mobile.discard()
      #  tofu.IDLER.next_round_tasks.append(remove_mobile2)
      waiter = Waiter(remove_mobile)
      waiter.wait_for(uid, 0)
      waiter.start()
      
    elif code == tofu.CODE_ADD_MOBILE:
      mobile_uid = struct.unpack("!i", data[1:5])[0]
      level_uid  = struct.unpack("!i", data[5:9])[0]
      print "* Tofu * Adding mobile %s in level %s..." % (mobile_uid, level_uid)
      def add_mobile(*args):
        mobile = tofu.Unique.getbyuid(mobile_uid)
        level  = tofu.Unique.getbyuid(level_uid )
        if not mobile in level.mobiles: mobile.level.add_mobile(mobile)
      waiter = Waiter(add_mobile)
      waiter.wait_for(mobile_uid)
      waiter.wait_for(level_uid)
      waiter.start()
      
    elif code == tofu.CODE_DATA_UNIQUE:
      print "* Tofu * Receiving unique..."
      unique = tofu.Unique.undump(data[1:])
      unique.received()
      assert (not hasattr(unique, "level")) or (not unique.level) or (unique.level in unique.level._alls2.values()), "Level sent with non-level unique !"
      self.arrived(unique)
      
    elif code == tofu.CODE_ENTER_LEVEL:
      uid = struct.unpack("!i", data[1:])[0]
      print "* Tofu * Entering level %s..." % uid
      #self.uids_arrival_planned.add(uid) # The server will send it
      # Previous level is outdated => drop it
      if tofu.Unique.hasuid(uid): tofu.Unique.getbyuid(uid).set_active(0)
      waiter = Waiter(lambda *uniques: None)
      waiter.wait_for(uid)
      waiter.start()
      
    elif code == tofu.CODE_ERROR:
      print "* Tofu * Server error: %s" % data[1:]
      #self.errors.append(data[1:])
      raise ClientServerError(data[1:])
      
  def arrived(self, unique):
    print "* Tofu * Received unique %s %s." % (unique.uid, unique)
    
    waiters = WAITERS.get(unique.uid)
    if waiters:
      for waiter in waiters: waiter.arrived(unique)
      del WAITERS[unique.uid]
    if hasattr(unique, "mobiles"):
      for mobile in unique.mobiles: self.arrived(mobile)
      
    self.uids_arrival_planned.discard(unique.uid)
      
  def ask_unique(self, uid):
    print "* Tofu * Ask for UID %s..." % uid
    self.uids_arrival_planned.add(uid)
    self.sendString(tofu.CODE_ASK_UNIQUE + struct.pack("!i", uid))
    
  def notify_action(self, mobile, action):
    self.sendString(tofu.CODE_DATA_ACTION + "%s%s" % (mobile.dumpuid(), action.dump()))
    
  def notify_add_mobile   (self, mobile): pass
  def notify_remove_mobile(self, mobile): pass
  
  def check_level_activity(self, level):
    for mobile in level.mobiles:
      if not mobile.controller.remote:
        level.set_active(1)
        return
      
    # No local user for this level => we can inactive it
    level.set_active(0)
    
  def notify_discard(self, unique):
    # The client NEVER saves data
    pass
  
  
  
class TCPFactory(ClientFactory):
  protocol = Notifier
  
  def clientConnectionFailed(self, connector, reason):
    m = reason.getErrorMessage()
    print "* Tofu * Connection failed:", m
    tofu.GAME_INTERFACE.network_error(m)
    
  def clientConnectionLost(self, connector, reason):
    m = reason.getErrorMessage()
    print "* Tofu * Connection lost:", m
    tofu.GAME_INTERFACE.network_error(m)
    
      
def serve_forever(host = "localhost", port = 6900, *args, **kargs):
  """serve_forever(host = "localhost", port = 6900, *ARGS, **KARGS)

Starts a game client, and connect to HOST on port PORT.
ARGS and KARGS are passed to GameInterface.__init__()."""
  #reactor.listenUDP(0, UDP())
  #twisted.internet.selectreactor.install()
  reactor = twisted.internet.reactor
  
  tofu.YourGameInterface(*args, **kargs)
  
  factory = TCPFactory()
  twisted.internet.reactor.connectTCP(host, port, factory)

  try:
    return tofu.IDLER.idle()
  finally:
    tofu.NOTIFIER.transport.loseConnection()
    tofu.IDLER.reactor.iterate()
    tofu.IDLER.reactor.runUntilCurrent()
    
    tofu.IDLER.reactor.removeAll()
    tofu.IDLER.reactor.disconnectAll()
    
    # We need to start the reactor in order to be able to stop it !!!
    # Else the program cannot ends normally.
    tofu.IDLER.reactor.callLater(0.0, reactor.stop); reactor.run()
    
