#!/usr/bin/env python
# encoding: utf-8
"""
pedronet.py

Created by Joachim Bengtsson on 2008-04-16.
Copyright (c) 2008 Third Cog Software. All rights reserved.
"""

import sys
import os
import types,time
from pedroclient import *


class PedroNetException(Exception):
  pass
  
  
def _Subscription__pobj_getattr(self, name):
  return "getting attr",name
  
class Subscription(object):
  """Holds a subscription. This class shud not be manually instanciated. 
  PedroNet uses this class to manage subscriptions. See PedroNet.subscribe"""
  def __init__(self, net, recipient, term, goal):
    self.net = net
    self.recipient = recipient
    self.term = term
    self.goal = goal
    self.id = None

  @property
  def rock(self):
    #Warning: rock must be 32-bit == wont work on 64bit python
    return id(self)
    
  def __call__(self, pobj):
    if isinstance(pobj, PStruct):
      pobj.populate_vars(self.term)
    
    func = self.recipient
    if type(self.recipient) == types.InstanceType: #TODO: don't know if this is the right way
      func = getattr(self.recipient, pobj.functor.val)
    func(pobj)
    
  def cancel(self):
    """Cancel the subscription"""
    self.net.unsubscribe(self)
    
  def __repr__(self):
    return "Subscription(%s, %s, %s, %s)"%(repr(self.net), repr(self.recipient), repr(self.term), repr(self.goal))

    
class AccessorMixin:
  def populate_vars(self, term):
    term = PedroParser().parse(term)
    self.argattr = {}
    for k,v in zip(term.args, self.args):
      if k.type == PObject.vartype:
        self.argattr[k.val] = v.val
    
  def __getattr__(self, attr):
    if self.argattr.has_key(attr):
      ret = self.argattr[attr]
    else:
      raise AttributeError, attr
    return ret

PStruct.__bases__ += (AccessorMixin,)

class NotifyBuilder(object):
  """Creates a notification. 
  Useage: 
    PedroNet.tell.functor(arg1,arg2,...,argN)
    generates and sends a notification
    "functior(arg1,arg2,...,argN)"
    
  Atoms are not supported. Use PedroNet.notify instead
  """
  def __init__(self, pnet, functor=""):
    self.net = pnet
    self.functor = functor
    
  def __call__(self, *args):
    ret = []
    for arg in args:
      if type(arg) in (types.IntType, types.FloatType):
        ret.append(str(arg))
      else:
        ret.append("\"%s\""%str(arg))
        
    ret = self.functor + "(" + ",".join(ret) + ")"
    self.net.notify(ret)
    
  def __getattr__(self, name):
    #TODO: validation
    self.functor = name
    return self
  
class PedroNet(object):
  """A Pedro wrapper that keeps track of subscriptions and dispatches them accordingly"""
  def __init__(self, net):
    super(PedroNet, self).__init__()
    self.net = net
    self.rockmap = {}  # rock => Subscription
    
  @property
  def tell(self):
    """Returns a NotifyBuilder object help(NotifyBuilder) for more info"""
    return NotifyBuilder(self)
    
  def subscribe(self, recipient, term, condition = "true"):
    """Subscribe to 'term' with 'condition', dispatching to 'recipient'"""
    subscription = Subscription(self, recipient, term, condition)
    rock = subscription.rock
    ret = self.net.subscribe(term, condition, rock)
    if not ret:
      err = "Subscription failed: '%s', condition '%s'"%(term, condition)
      raise err
    self.rockmap[rock] = subscription
    subscription.id = ret
    return subscription
  
  def unsubscribe(self, subscription):
    if self.net.unsubscribe(subscription.id) == subscription.id:
      rock = subscription.rock
      del self.rockmap[rock]
      
  def unsubscribe_all(self, recipient = None):
    """if recipient == None, unsibscribe everything for this client.
      else unsubscribe all for that recipient"""
    if recipient:
      subs = [sub for sub in self.rockmap.values() if sub.recipient == recipient]
    else:
      subs = self.rockmap.values()
      
    for sub in subs:
      self.unsubscribe(sub)
  
  def notify(self, term):
    ret = self.net.notify(term)
    if not ret:
      raise PedroNetException("Notification failed")
  
  def hasTerm(self):
    return self.net.notification_ready()
  
  def poll(self):
    """Retrieves the next term from pedro or blocks if there isn't one."""
    term, rock = self.net.get_term()
    subscription = self.rockmap.get(rock, None)
    if subscription:
      subscription(term)
    #XXX: maybe shud not raise in the event that notification arrives after unsubscription is processed
    else:
      raise PedroNetException("Subscription not found for rock %d"%rock)
    
  
#Test notification handler class
class Test:
  def message(self, a):
    print "Test::message", a.Message
    print
    
  #shud not be called because we will unsubscribe before notification reaches us
  def notcalled(self, a):
    print "Test::notcalled",a
    print
    
def test():
  #Create PedroNet object with a PedroClient
  pnet = PedroNet(PedroClient('voxar.net'))
    
  #test notification handler
  def dosomething(a):
    print "::dosomething",a.What, a.At
    print
  
  #subscribe to some messages
  testobject = Test()
  pnet.subscribe(testobject, "message(Message)")
  pnet.subscribe(dosomething, "dosomething(What, At)")
  sub = pnet.subscribe(testobject, "notcalled(Hej)")
  
  #display some info about a subscription
  print "Subscription:", repr(sub.term)
  print "   recipient:",sub.recipient
  print "        rock:",sub.rock
  print "        goal:",repr(sub.goal)
  print "          id:",sub.id
  
  
  
  #cancel a subscription
  #sub.cancel()
  #or
  #pnet.unsubscribe(sub)
  #or
  #pnet.unsubscribe_all(testobject)

  #send notifications
  pnet.notify('message("Yo kid")')
  #or
  pnet.tell.notcalled("hej")
  pnet.tell.dosomething("Eat a banana", "NOW")
  
  
  for i in range(5):
    try:
      if pnet.hasTerm(): pnet.poll()
      time.sleep(0.01)
    except Exception,e:
      print e
  pnet.net.disconnect()
    
if __name__ == "__main__":
  test()