import string
import types
try:
    import cPickle
    pick_mod=cPickle
except:
    import pickle
    pick_mod=pickle
import socket
import atexit
"""**
   * comm.py is the commonality between Sython and sythond.  Communication
   * between the two applications from the Sython side is done via this module.
   * @author Michael Gaiman
   * @version 1 2/15/2005
   **"""

"""Daemon port"""
dPort = 60123
"""Daemon host"""
dHost = 'localhost'
_CC = None #the current Client Code
_VERSION = 1
_VALID_COMMANDS = ('NEW_SESSION','NEW_VAR','OP1','OP2','OPmL','OPmR','FREE_REF',
    'SY_VAL','END_SESSION','FILE_INPUT','FILE_OUTPUT','DB_QUERY','PRINT_DEBUG')
_MIN_DIST=10
#tuple containing all of the valid vsets for newVariable
#Note: when changing the order or values of VSETS, changes must be reflected in
#newVariable.
VSETS = ('ALL', #can be from any set
    'EVEN', #even numbers only
    'ODD', #odd numbers only
    'POSITIVE', #numbers greater than zero only
    'NEGATIVE', #negative numbers only
    'UPPER', #uppercase letters
    'LOWER', #lowercase letters
    'PUNCT', #punctuation
    'SPACE', #spaces
    'DIGITS' #numerical digits
)

#valid operations:
_VALID_OPS=('+','*','len','-','%','/','pow','abs')

class Error(Exception):
    """Base class for exceptions in this module."""
    pass

def _checkID(syvarID):
    """checks to make sure the supplied sython variable ID is valid"""
    if type(syvarID) is not types.StringType or syvarID=='':
        raise TypeError, 'String expected for syvarID'
    #is is in form 'syXXXXXXXX' where X is an int of variable length
    if len(syvarID)<3 or syvarID[:2]!='sy':
        raise ValueError, 'Invalid syvarID'
    num=syvarID[2:]
    try:
        num=int(num)
    except:
        raise ValueError, 'Invalid syvarID'
    if num<0: #must be greater than 0, no negative numbers...
        raise ValueError, 'Invalid syvarID'

def _checkOP(op):
    """check to make sure the the supplied operation is valid
    in a general sense"""
    if type(op) != types.StringType:
        raise TypeError, 'String expected for op'
    if op not in _VALID_OPS:
        raise ValueError, 'Invalid operation'

def _debug():
    """prints debugging information in syd log"""
    _sendMessage('PRINT_DEBUG',{})

def newSession():
    """Start a new Sython communication session.
    This method should be called before any others.
    An exception is raised on failure."""
    response=_sendMessage('NEW_SESSION',{})
    #exceptions are just propegated back up...
    global _CC
    _CC = response
    #atexit.register(endSession)

def hasSession():
    """Returns True if a session has already been started.
    False otherwise."""
    return _CC != None
    
def newVariable(vtype, vrange=None, vset=('ALL',)):
    """Declare a new sython variable.
    vtype is a string that can contain 'i' or 's'
    vrange is an optional argument that, when vtype is 'i', accepts a tuple
    with a starting and ending value and when vtype is 's' accepts an int for
    the size of the string.  
    vset is an optional argument that accepts a tuple containing
    strings from the VSETS tuple.
    A sython variable ID is returned"""
    return _sendMessage('NEW_VAR',{'vtype':vtype,'vrange':vrange,'vset':vset})

def op(op, syvarID):
    """Perform an operation on a sy variable.
    This version of op supports single argument operations"""
    _checkID(syvarID)
    _checkOP(op)
    return _sendMessage('OP1',{'var1':syvarID, 'op':op})

def op2Sy(op, syvarID1, syvarID2):
    """Perform an operation with two sy variables.
    This version of op supports two argument operations"""
    _checkID(syvarID1)
    _checkID(syvarID2)
    _checkOP(op)
    return _sendMessage('OP2',{'var1':syvarID1, 'var2':syvarID2, 'op':op})
    
def op2MixedL(op, syvarID, arg2):
    """Perform an operation with one sy variable and one non sy variable
    The sython variable is the one on the left."""
    _checkID(syvarID)
    _checkOP(op)
    if type(arg2) not in (types.IntType, types.LongType, types.StringType):
        raise SySecurityError, 'Cannot perform operations between sython and non int or string variables'
    return _sendMessage('OPmL',{'var1':syvarID, 'arg2':arg2, 'op':op})

def op2MixedR(op, syvarID, arg2):
    """Perform an operation with one sy variable and one non sy variable
    The sython variable is the one on the right."""
    _checkID(syvarID)
    _checkOP(op)
    if type(arg2) not in (types.IntType, types.LongType, types.StringType):
        raise SySecurityError, 'Cannot perform operations between sython and non int or string variables'
    return _sendMessage('OPmR',{'var1':syvarID, 'arg2':arg2, 'op':op})

def inputFromFile():
    """Read a line of input from the sythond INPUT_FILE."""
    return _sendMessage('FILE_INPUT',{})

def outputToFile(syvarID):
    """Writes the sython variable to a new line in sythond OUTPUT_FILE."""
    _checkID(syvarID)
    return _sendMessage('FILE_OUTPUT',{'var':syvarID})

def queryDB(sql,*args):
    """Query's sythond's database with the sql statement passed.
    Arguments should be in the form of (Boolean,Object) pairs where the boolean
    is true when the Object is a syvarID and false otherwise."""
    if type(sql) != types.StringType:
        raise TypeError, 'Invalid SQL statement'
    for i in args:
        _checkID(i)
    return _sendMessage('DB_QUERY',{'sql':sql,'args':args})
    
def freeRef(syvarID):
    """Decrease the reference count for the sython variable specified."""
    _checkID(syvarID)
    return _sendMessage('FREE_REF',{'var':syvarID})

def syval(syvarID):
    """Retrieve the current syval for the supplied sython variable."""
    _checkID(syvarID)
    return _sendMessage('SY_VAL',{'var':syvarID})

def endSession():
    """Notifies sythond that this session is over, so that it can free
    the resources used by this client"""
    global _CC
    if _CC:
        _sendMessage('END_SESSION',{})
        _CC=None

def _sendMessage(cmd,data):
    """Send a message to the sythond.  Generally
    command specific methods should be called instead
    of sendMessage directly."""
    
    #perform some error checking
    # let the daemon deal with it...
    #if cmd not in _VALID_COMMANDS:
    #    raise Error, "Invalid communication command"
    #if type(data) not in (types.DictType, types.NoneType):
    #    raise TypeError, "Invalid data variable"
    message={'V':_VERSION, 'CC':_CC, 'CMD':cmd, 'data':data}
    sock=recv=None
    try:
        sock=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect((dHost,dPort))
        recv=sock.makefile('r')        
        sock.send(pick_mod.dumps(message))
        received=pick_mod.load(recv)
        if type(received) == types.StringType and received[0:7] == 'ERROR -':
            raise Error, received[7:]
        return received
    finally:
        if recv:
            recv.close()
        if sock:
            sock.close()