# -*- coding: utf-8 -*-

"""
This Module holds all caches and buffers for network management and the needed logic for managing them
 """
import thread
import random
import time 

class KnownPeers:
    """The central Cache holding all known users for name resolution.
 
     Functions include logic for cache management (adding new known peers, removing old ones, prioritisation) and for user lookup (search)"""
    __cache = {}    #cache is a dictionary in the form {'MURRAY-ID': (IP-Version, IP, Port)}
    __cachelock = thread.allocate_lock()    #lock guarding __cache
    __priorityList = []    #list of priorities of murray ids
    __priorityListLock = thread.allocate_lock()
    
    def __writeToCache(self, key, value):
        """Back-End method for thread-safe writing to the cache
        
        returns nothing"""
        self.__cachelock.acquire()
        try:
            self.__cache[key] = value
        finally:
            self.__cachelock.release()
    
    def __readFromCache(self, key):
        """Back-End method for thread-safe reading from the cache
        
        returns the value associated to key"""
        self.__cachelock.acquire()
        try:
            value = self.__cache[key] if self.__cache.has_key(key) else None
        finally:
            self.__cachelock.release()
        return value
    
    def __deleteFromCache(self, key):
        """Back-End method for thread-safe deletion of a single entry from the cache
        
        returns nothing"""
        self.__cachelock.acquire()
        try:
            if self.__cache.has_key(key):
                del self.__cache[key]
        finally:
            self.__cachelock.release()
            
    def __getPrioList(self):
        """returns a copy of the current list
        """
        self.__priorityListLock.acquire()
        try:
            returnList = self.__priorityList
        finally:
            self.__priorityListLock.release()
        return returnList
        
    def __writeToPrioList(self, murray_id):
        """adds a new MURRAY-ID to the "top" of the list.
        
        returns nothing"""
        self.__priorityListLock.acquire()
        try:
            self.__priorityList.append(murray_id)
        finally:
            self.__priorityListLock.release()
            
    def __deleteFromPrioList(self, murray_id):
        """removes a MURRAY-ID from the Priority-List
        
        returns nothing"""
        self.__priorityListLock.acquire()
        try:
            self.__priorityList.remove(murray_id)
        finally:
            self.__priorityListLock.release()
            
    
    
    def add(self, murray_id, ip_version, ip, port):
        """adds a new known peer to the list
        
        returns nothing"""
        self.__writeToCache(murray_id, (ip_version, ip, port))
        # the new peer also has to be added to the PrioList
        self.__writeToPrioList(murray_id)
        
                 
    def update(self, murray_id, ip_version=None, ip=None, port=None):
        """updates the data on a known peer
        
        returns nothing"""
        if self.known(murray_id):
            old_data = self.__readFromCache(murray_id)
            new_data = list(old_data)
            if ip_version:
                new_data[0] = ip_version
            if ip:
                new_data[1] = ip
            if port:
                new_data[2] = port
            self.__writeToCache(murray_id, tuple(new_data))
            # updated peers move to the top of our PrioList
            self.toTop(murray_id)
        else:
            self.add(murray_id, ip_version, ip, port)
        
    def remove(self, murray_id):
        """Removes a peer from the known peers list
            
        returns nothing"""
        self.__deleteFromCache(murray_id)
        # the peer has to be removed from the PrioList, too
        self.__deleteFromPrioList(murray_id)
            
    def getByMurray_ID(self, murray_id):
        """gets the network information of a known peer
        
        returns {ip_version: , ip: , port: }"""
        value = self.__readFromCache(murray_id)
        returnDict = {'ipversion':value[0], 'ip':value[1], 'port':value[2]}
        return returnDict
                
    def saveOnShutdown(self, filename = 'known_peers.dict'):
        """stores the list of known peers in a persistent file
        
        returns nothing"""
        
        import cPickle        #Pickling rules...
        
        fileToSave = file(filename, 'w')
        self.__cachelock.acquire()
        try:
            cPickle.dump(self.__cache, fileToSave)
        finally:
            self.__cachelock.release()
            fileToSave.close()    
        
    def restoreOnStartup(self, filename = 'known_peers.dict'):
        """restrores the cache of known peers on startup of murrayb
        
        returns nothing"""
        
        import cPickle
        
        self.__cachelock.acquire()
        try:
            fileRestoreFrom = file(filename, 'r')
            self.__cache = cPickle.load(fileRestoreFrom)
            fileRestoreFrom.close()
            self.fillPrioList(self.__cache.keys())
        except IOError:
            pass
        finally:
            self.__cachelock.release()
        
        
    def known(self, murray_id):
        """determines, if a requested murray_id is already in the known_peers cache.
        
        returns True or False"""
        try:
            self.__readFromCache(murray_id)
            return True
        # If the given murray-id is not in the cache, __read() will throw a 
        # KeyError. In this case, we return false.
        except KeyError:
            return False
    
    def fillPrioList(self, list_of_keys):
        """fills the priorityList with all known peers after startup
        
        returns nothing"""
        self.__priorityListLock.acquire()
        try:
            self.__priorityList = list_of_keys
        finally:
            self.__priorityListLock.release()
    
    def toTop(self, murray_id):
        """moves an entry to the top of the list
        
        returns nothing"""
        self.__deleteFromPrioList(murray_id)    #first delete...
        self.__writeToPrioList(murray_id)        #then add to the top of the list
        
    def getMurray_IDByPriority(self, begin = 0, count = 1):
        """gets a given number of MURRAY-IDs from the list sorted by descending priority
        
        begin: 0 means "ID with the highest priority"
        count: self-explaining
        
        returns a list of murray-ids, length count, defaults to top of the list"""
        returnList = self.__getPrioList()
        lengthOfPriorityList = len(returnList)
        if lengthOfPriorityList == 0:
            return None
        ### print lengthOfPriorityList
        # since the murray_id with the highest priority is always the last item on the list, we need some math
        while begin < 0:
            begin += lengthOfPriorityList
        while begin > lengthOfPriorityList:
            begin -= lengthOfPriorityList
        end = lengthOfPriorityList - begin
        start = end - count
        ### print start, end
        if start > 0:
            returnList = returnList[start:end]
        else:
            returnList = returnList[:end]
        #just need to do some little reversing-action...
        returnList.reverse()
        return returnList
        
    def getByPriority(self, begin = 1, count = 1):
        """gets the information on count given peers selected by their priority, starting with begin
        
        returns a subset of the known peers cache"""
        liste = self.getMurray_IDByPriority(begin, count)
        returnDict = {}
        for item in liste:
            tmp = self.getByMurray_ID(item)
            returnDict[item] = tmp 
        return returnDict
        
    def getAllMurray_IDs(self):
        """gets all known Murray IDs in order of their priority"""
        returnList = self.__getPrioList()
        returnList.reverse()
        return returnList
    
    def getAll(self):
        """returns the complete cache"""
        self.__cachelock.acquire()
        tmp = None
        try:
            tmp = self.__cache
        finally: 
            self.__cachelock.release()
        return tmp
            
    
    def clean(self, threshhold = 5000000):
        """cleans the prioritylist and Known Peers cache once there are more than threshhold entries
        
        returns nothing"""
        workingList = self.getAllMurray_IDs()
        delTuple = ()
        length = len(workingList)
        if length > threshhold:
            delTuple = tuple(workingList[:length - threshhold])
            for item in delTuple:
                self.remove(item)
        else:
            pass
        
    
class FIFO(list):
    """This class implements a First-In-First-Out Version of the standard list dataype."""
    def __init__(self, *sequence):
        args = list(sequence)
        args.reverse()
        list.__init__(self, args)
    
    def pop(self, index = 0):
        """delete and return item at index - default first item
        
        returns list item"""
        try:
            tmp = self.__getitem__(index)
            self.__delitem__(index)
        except IndexError:
            return None
        return tmp
    
class FIFOBuffer:
    """This class holds a FIFO-Buffer"""
    __buffer = FIFO()
    __bufferLock = thread.allocate_lock()
    
    def add(self, xmpp_object):
        """appends a xmpp object to the end of the list.
        
        returns nothing"""
        self.__bufferLock.acquire()
        try:
            self.__buffer.append(xmpp_object)
        finally:
            self.__bufferLock.release()
    
    def get(self):
        """returns the first object in the list"""
        self.__bufferLock.acquire()
        returnObject = None
        try:
            returnObject = self.__buffer.pop()
        finally:
            self.__bufferLock.release()
            
        return returnObject

class Waiting:
    """Holds all messages waiting for information which has to be collected 
    from the network"""
    
    __buffer = {}
    __bufferLock = thread.allocate_lock()
    
    def __write(self, key, value):
        """adds a new item to the buffer
        
        returns nothing"""
        self.__bufferLock.acquire()
        try:
            self.__buffer[key] = value
        finally:
            self.__bufferLock.release()
        
    def __read(self, key):
        """returns a value from the buffer"""
        self.__bufferLock.acquire()
        try:
            returnValue = self.__buffer[key]
        finally:
            self.__bufferLock.release()
        return returnValue
    
    def __delete(self, key):
        """delete a waiting item from the buffer
        
        returns nothing"""
        self.__bufferLock.acquire()
        try:
            del self.__buffer[key]
        finally:
            self.__bufferLock.release()
    
    def add(self, typeOfRequest, request):
        """appends a message to the buffer
        
        returns random number for identification of responses"""
        
        while True:
            requestID = random.randint(0, 2**16)
            #check, if a request with the same ID is already in the buffer, and if yes, create a new ID
            try: 
                self.get(requestID)
            except KeyError:
                # create timestamp
                timestamp = int(time.time())
                self.__write(requestID, (timestamp, typeOfRequest, request))
                return requestID
            
        
    def get(self, requestID):
        """gets the original request for the response with requestID
        
        returns (typeOfRequest, request)"""
        returnValue = self.__read(requestID)
        #we don't need no edu... timestamp
        returnValue = (returnValue[1], returnValue[2])
        return returnValue
    
    def remove(self, requestID):
        """removes the entry with given requestID from the waiting List.
        
        returns nothing"""
        self.__delete(requestID)
    
    def cleanUp(self, timeOut = 600):
        """cleans the buffer of all waiting requests older than timeOut secons (defaults to 10 minutes)
        
        returns number of deleted requests"""

        self.__bufferLock.acquire()
        try:
            workingDict = self.__buffer
        finally:
            self.__bufferLock.release()
        
        reftime = int(time.time())
        
        delList = []
        for item in workingDict.iteritems():
            if (reftime - item[1][0]) > timeOut:
                delList.append(item[0])
        retList = []
        for item in delList:
            try:
                KnownPeers().remove(self.get(item)[1]['destination']['uid'])
            except ValueError:
                pass
            entry = self.get(item)
            if entry[1]['type'] == 'userdata':
                thread.start_new_thread(xmppserver.informUser, ('Message could not be sent: (Peer unreachable)' + str(entry[1]),))
                self.remove(item)
            elif entry[1]['type'] == 'usersearch':
                retList.append(entry[1])
            else:
                self.remove(item)
            
        return retList
            
class Buffer:
    """This class holds a generic buffer"""
    __buffer = {}
    __bufferLock = thread.allocate_lock()
    
    def add(self, key, value):
        """appends a xmpp object to the end of the list.
        
        returns nothing"""
        self.__bufferLock.acquire()
        try:
            self.__buffer[key] = value
        finally:
            self.__bufferLock.release()
    
    def get(self, key):
        """returns the first object in the list"""
        self.__bufferLock.acquire()
        returnObject = None
        try:
            returnObject = self.__buffer.pop(key)
        finally:
            self.__bufferLock.release()
            
        return returnObject
        
    def remove(self, key):
        """GUESS!"""
        self.__bufferLock.acquire()
        try:
            del self.__buffer[key]
        except KeyError:
            pass
        finally:
            self.__bufferLock.release()
            
            
"""TEST-Routine for Debugging -- IGNORE THIS!!!! NO Documentation!!!"""
if __name__ == '__main__':
    import random
    print '==============================================='
    print 'Testing KnownPeers class'
    print '==============================================='
    x = KnownPeers()
    testdata = []
    for i in range(5000):
        r = random.randint(0, 99999)
        while r in testdata:
            r = random.randint(0, 999999)
        testdata.append((r, 4, '127.0.0.1', 4711))
    print 'add()'
    for i in testdata:
        x.add(i[0], i[1], i[2], i[3])
    print 'done'
    print 'update'
    testdata = []
    for i in range(5000):
        r = random.randint(0, 99999)
        while r in testdata:
            r = random.randint(0, 999999)
        testdata.append((r, 4, '127.0.0.1', 4711))
    for i in testdata:
        x.update(i[0], i[1], i[2], i[3])
    print 'done'
    print 'remove'
    x.remove(testdata[5][0])
    print 'done'
    print 'getByMurray_ID()'
    print x.getByMurray_ID(testdata[7][0])
    print 'done'
    print 'saveOnShutdown'
    x.saveOnShutdown()
    print 'done'
    print 'restoreOnStartup'
    x.restoreOnStartup()
    print 'done'
    print 'known'
    print x.known(testdata[0][0])
    print x.known(testdata[5][0])
    print 'ok'
    print 'getting Murray_ID of prio 0'
    mid = x.getMurray_IDByPriority()
    print mid, x.getByMurray_ID(mid[0])
    print 'getting Murray_IDs of prio 7-13'
    mid = x.getMurray_IDByPriority(6, 6)
    print mid
    print 'getting last Murray_ID'
    mid = x.getMurray_IDByPriority(-1)
    print mid
    print 'done'
    print 'getting dataof prio 8'
    print x.getByPriority(8)
    print 'done'
    print 'getting all murray-ids'
    print len(x.getAllMurray_IDs())
    print len(x.getAll()), x.getAll()[testdata[8][0]]
    print 'finally: cleaning up'
    x.clean(100)
    print len(x.getAll())
    print 'done!!!'
    print '==========================================='
    print 'Testing FIFO Buffer'
    print '==========================================='
    x = FIFOBuffer()
    testdata = []
    for i in range(5000):
        r = random.randint(0, 99999)
        while r in testdata:
            r = random.randint(0, 999999)
        testdata.append(r)
    for i in testdata:
        x.add(i)
    print 'Buffer Filled'
    print 'Last element:', x.get()
    print 'Should be:', testdata[0]
    print 'Hooray!!!'
    print '==========================================='
    print 'Testing Waiting List'
    print '==========================================='
    print 'Adding request'
    req = ('nix besonneres', {'destination':{'uid': mid[0]}, 'type' : 0})
    print req
    x = Waiting()
    id = x.add(req[0], req[1])
    print 'getting request', id
    print x.get(id)
    print 'removing', id
    x.remove(id)
    try:
        print x.get(id)
        print 'This shouldn\'t work'
    except:
        print 'Horray! re-adding it'
    id = x.add(req[0], req[1])
    print 'waiting for 5 secs'
    time.sleep(5)
    x.cleanUp(1)
    try:
        print 'getting' , id, x.get(id)
        print 'This shouldn\'t work'
    except:
        print 'Horray!'
    
    
    