
class HashedIdxList:
    
    """
    The hasehd index list is a special data type that can be indexed
    both with a character hash and an integer index
    
    data = [val_0,    val_1,    ..., val_n]
              ^          ^             ^
    hash = [hash_0,  hash_1, ...,   hash_n]
    
    then, data[1] yields val_1, but data['hash_0'] also yields val_1.
    Searching for a hash in the hashmap can be expensive.
    The decision was thus to search for the hash in __setitem__ instead of __getitem__
    """
    
    use_str_hashes = True 

    def __init__(self, hashes: list, data: list = None, default_val = None):
        """
        :param hashes: the initial hashes
        :param data: the initial data
        :param default_val: default value (should be None, float or a generator/function)
        """

        if data is None:
            data = []

        self.data = data 
        if default_val is None:
            self.default_val = lambda: 0.0 # default_val
        else:
            self.default_val = default_val

        assert len(data) == len(hashes)
        self.map = {hashes[i]: i for i in range(len(data))}

    @property
    def hashes(self):
        return list(self.map.keys())
    
    def get_value_by_idx(self, k):
        if k < len(self.data):
            return self.data[k]
        else:
            return self.default_val()

    def __getitem__(self, k):
        """
        retrieve an item from the 
        :param k: str or int, index to the item
        """

        if (not self.__class__.use_str_hashes) or k == 0:
            return self.get_value_by_idx(k)

        else:
            if k in self.map:
                return self.data[self.map[k]]    
            else:
                return self.default_val()
        
        raise TypeError("Could not identify key type '%s'" % (type(k)))
        
    
    def set_value_by_idx(self, k, v):
        if k >= len(self.data):
            self.append(v, "hash_%03i" % (len(self.data)-1)) # no hash given? just take str of index
        else:
            self.data[k] = v 
        return 
    
    def __setitem__(self, k, v):
        """
        set a value v for an item k
        :param k: key (str or )
        :param v: 
        """
        
        if (not __class__.use_str_hashes) or (k == 0 or k == 1 or k == 2):
            self.set_value_by_idx(k, v)
        else:
            if k not in self.map:
                self.append(v, k)
            else:
                self.data[self.map[k]] = v 
            return 

    def append(self, data, hash):
        """
        append data item using data-hash tuple 
        """
        self.data.append(data)
        self.map[hash] = len(self.data)-1
        assert len(self.data) == len(self.map)

    def __str__(self):
        s = ""
        for i, k in enumerate(list(self.map.keys())):
            s += " ".join([str(i), ":", "'%s'" % k, "-->", str(self.data[self.map[k]])]) + "\n"
        return s.strip()
    
    def items(self):
        """
        get the items just as in a normal Python dict (hash->value)
        """
        # TODO return an iterator instead of whole list 
        return [(hash, self.data[self.map[hash]]) for hash in self.hashes]

if __name__ == "__main__": 
    
    data = [[0,0,0]]
    hashes = ["Total"] 
    l = HashedIdxList(hashes, data)

    l["C"] = [0,3,4]
    l["C"] = [0,3,5]
    print(str(l))