#!/usr/bin/env python
#
# This library is free software, distributed under the terms of
# the GNU Lesser General Public License Version 3, or any later version.
# See the COPYING file included in this archive
#
# The docstrings in this module contain epytext markup; API documentation
# may be created by processing this file with epydoc: http://epydoc.sf.net

import hashlib

from twisted.internet import defer

import kademlia.node
from kademlia.node import rpcmethod


class EntangledNode(kademlia.node.Node):
    """ Entangled DHT node
    
    This is basically a Kademlia node, but with a few more (non-standard, but
    useful) RPCs defined.
    """
    def __init__(self, id=None, udpPort=4000, dataStore=None, routingTable=None, networkProtocol=None):
        kademlia.node.Node.__init__(self, id, udpPort, dataStore, routingTable, networkProtocol)
        self.invalidKeywords = []
        self.keywordSplitters = ['_', '.', '/']

    def searchForKeywords(self, keywords):
        """ The Entangled search operation (keyword-based)
        
        Call this to find keys in the DHT which contain the specified
        keyword(s).
        """
        if type(keywords) == str:
            for splitter in self.keywordSplitters:
                keywords = keywords.replace(splitter, ' ')
            keywords = keywords.lower().split()

        keyword = None
        for testWord in keywords:
            if testWord not in self.invalidKeywords:
                keyword = testWord
                break
        if keyword == None:
            df = defer.Deferred()
            df.callback([])
            return df
        
        keywords.remove(keyword)

        h = hashlib.sha1()
        h.update(keyword)
        key = h.digest()
        
        def checkResult(result):
            if type(result) == dict:
                # Value was found; this should be list of "real names" (not keys, in this implementation)
                index = result[key]
                filteredResults = list(index)
                # We found values containing our first keyword; Now filter for the rest
                for name in index:
                    for kw in keywords:
                        if name.lower().find(kw) == -1:
                            filteredResults.remove(name)
                index = filteredResults
            else:
                # Value wasn't found
                index = []
            return index
 
        df = self.iterativeFindValue(key)
        df.addCallback(checkResult)
        return df

    def publishData(self, name, data):
        """ The Entangled high-level data publishing operation
        
        Call this to store data in the Entangled DHT.
        
        @note: This will automatically create a hash of the specified C{name}
        parameter, and add the published data to the appropriate inverted
        indexes, to enable keyword-based searching. If this behaviour is not 
        wanted/needed, rather call the Kademlia base node's
        C{iterativeStore()} method directly.
        """
        h = hashlib.sha1()
        h.update(name)
        mainKey = h.digest()

        outerDf = defer.Deferred()

        def publishKeywords(deferredResult=None):        
            # Create hashes for the keywords in the name
            keywordKeys = self._keywordHashesFromString(name)
            # Update the appropriate inverted indexes
            df = self._addToInvertedIndexes(keywordKeys, name)
            df.addCallback(lambda _: outerDf.callback(None))

        # Store the main key, with its value...
        df = self.iterativeStore(mainKey, data)
        
        df.addCallback(publishKeywords)
        
        return outerDf

    def _addToInvertedIndexes(self, keywordKeys, indexLink):
        # Prepare a deferred result for this operation
        outerDf = defer.Deferred()

        kwIndex = [-1] # using a list for this counter because Python doesn't allow binding a new value to a name in an enclosing (non-global) scope

        # ...and now update the inverted indexes (or add them, if they don't exist yet)
        def addToInvertedIndex(results):
            kwKey = keywordKeys[kwIndex[0]]
            if type(results) == dict:
                # An index already exists; add our value to it
                index = results[kwKey]
                #TODO: this might not actually be an index, but a value... do some name-mangling to avoid this
                index.append(indexLink)
            else:
                # An index does not yet exist for this keyword; create one
                index = [indexLink]
            df = self.iterativeStore(kwKey, index)
            df.addCallback(storeNextKeyword)

        def storeNextKeyword(results=None):
            kwIndex[0] += 1
            if kwIndex[0] < len(keywordKeys):
                kwKey = keywordKeys[kwIndex[0]]
                # We use the find algorithm directly so that kademlia does not replicate the un-updated inverted index
                if kwKey in self._dataStore:
                    df = defer.Deferred()
                    df.callback({kwKey: self._dataStore[kwKey]})
                else:
                    df = self._iterativeFind(kwKey, rpc='findValue')
                df.addCallback(addToInvertedIndex)
            else:
                # We're done. Let the caller of the parent method know
                outerDf.callback(None)

        if len(keywordKeys) > 0:
            # Start the "keyword store"-cycle
            storeNextKeyword()
        else:
            outerDf.callback(None)

        return outerDf

    def removeData(self, name):
        """ The Entangled high-level data removal (delete) operation
        
        Call this to remove data from the Entangled DHT.
        
        @note: This will automatically create a hash of the specified C{name}
        parameter. It will also remove the published data from the appropriate
        inverted indexes, so as to maintain reliability of keyword-based
        searching. If this behaviour is not wanted/needed, rather call this
        node's C{iterativeDelete()} method directly.
        """
        h = hashlib.sha1()
        h.update(name)
        mainKey = h.digest()
        
        # Remove the main key
        self.iterativeDelete(mainKey)

        # Create hashes for the keywords in the name
        keywordKeys = self._keywordHashesFromString(name)
        
        # Update the appropriate inverted indexes
        df = self._removeFromInvertedIndexes(keywordKeys, name)
        return df

    def _removeFromInvertedIndexes(self, keywordKeys, indexLink):
        # Prepare a deferred result for this operation
        outerDf = defer.Deferred()
    
        kwIndex = [-1] # using a list for this counter because Python doesn't allow binding a new value to a name in an enclosing (non-global) scope

        # ...and now update the inverted indexes (or ignore them, if they don't exist yet)
        def removeFromInvertedIndex(results):
            kwKey = keywordKeys[kwIndex[0]]
            if type(results) == dict:
                # An index for this keyword exists; remove our value from it
                index = results[kwKey]
                #TODO: this might not actually be an index, but a value... do some name-mangling to avoid this
                try:
                    index.remove(indexLink)
                except ValueError:
                    df = defer.Deferred()