# ranks.py
"""
Branches and ranks objects.

Objects for representing the ranks of a hierarchy, with the 
possibility of branching at nodes with redefined ranks via
the 'project_schema' directives in project YAML files.
"""

import numbers

class Branch(object):
    """
    Branch represents a ranking system in the tree of schemas.
    
    It takes the name of the project_unit where the ranking system
    is overridden, and controls how all ranks defined within it
    are interpreted.
    """
    def __init__(self, parent, code, start, ranks):
        self.parent = parent
        self.code = str(code)
        self.start = int(start)
        
        self._ranks = RankList(self,[])
        if parent:
            for rank in parent.ranks:
                if int(rank) < start:
                    self._ranks.append(rank)
            
        for num, name in enumerate(ranks):
            rank = Rank(self, num + start, name)
            self._ranks.append(rank)
            
    def __repr__(self):
        ranklist = ', '.join([str(r) for r in self.ranks[1:]])
        if self.code:
            code = self.code
        else:
            code = 'trunk'
        return "<branch '%s': %s>" % (code, ranklist)
    
    def __contains__(self, other):
        if isinstance(other, Rank) and other in self._ranks:
            return True
        else:
            return False
            
    def rank(self, n):
        """
        Coerce int or string to rank, if it matches a rank in this Branch.
        """
        if isinstance(n, int) and 0 < n < len(self._ranks):
            return self._ranks[n]
        elif isinstance(n, str):
            for rank in self._ranks:
                if str(rank) == n:
                    return rank
        else:
            raise TypeError
            
    @property
    def ranks(self):
        # Read-only property.
        return self._ranks
    

            
class Rank(object):
    """
    Ranks are named numbers indicating the position in a hierarchy.
    
    They can be incremented and decremented. The value 0 represents the top
    rank, with increasing numbers indicating finer grades in taxonomic rank.
    
    They can have integers added to or subtracted from them, meaning to go
    down or up in rank. They cannot be added to each other, though. Note that
    higher and lower rank in the real world since of more or less scope is
    inverted as a number -- the lower rank has a higher number.
    
    There are upper and lower bounds to rank, defined by the schema 'Branch'.
    
    Coercing a rank to an integer (int()) returns the numeric rank.
    
    Coercing a rank to a string (str()) returns the rank name.
    
    The representation includes the branch name and rank name.
    
    Ranks can be subtracted, returning an integer representing the difference
    in rank.
    """
    def __init__(self, branch, num, name):
        self.num = num
        self.name = name
        self.branch = branch
        
    def __index__(self):
        return self.num
        
    def __int__(self):
        return self.num
    
    def __str__(self):
        return self.name
    
    def __repr__(self):
        return '<%s:%d-%s>' % (self.branch.code, self.num, self.name)
    
    def __hash__(self):
        return hash((self.branch, self.num, self.name))
    
    def __eq__(self, other):
        if isinstance(other, Rank):
            if hash(self) == hash(other):
                return True
            else:
                return False
        elif isinstance(other, str):
            if other == self.name:
                return True
            else:
                return False
        elif isinstance(other, int):
            if other == self.num:
                return True
            else:
                return False
        else:
            return False
        
    def __gt__(self, other):
        if isinstance(other, Rank):
            if self.num > other.num:
                return True
            else:
                return False
        elif isinstance(other, int):
            if self.num > other:
                return True
            else:
                return False
        else:
            raise TypeError("Rank can't compare to %s" % type(other))
        
    def __lt__(self, other):
        if isinstance(other, Rank):
            if self.num < other.num:
                return True
            else:
                return False
        elif isinstance(other, int):
            if self.num < other:
                return True
            else:
                return False
        else:
            raise TypeError("Rank can't compare to %s" % type(other))
        
    def __ge__(self, other):
        return (self > other or self == other)
    
    def __le__(self, other):
        return (self < other or self == other)

        
    def __add__(self, other):
        if isinstance(other, int):
            if (self.num + other) < len(self.branch.ranks):
                return self.branch.ranks[self.num+other]
            elif (self.num + other) < 1:
                return trunk
            else:
                return None
        else:
            raise TypeError("Changes in rank must be integers.")
        
    def __radd__(self, other):
        return self.__add__(other)
    
    def __sub__(self, other):
        if isinstance(other, Rank):
            return (self.num - other.num)
        elif isinstance(other, int):
            if 0 < (self.num - other) < len(self.branch.ranks):
                return self.branch.ranks[self.num-other]
            elif (self.num - other) < 1:
                return trunk
            elif (self.num - other) > len(self.branch.ranks):
                return None
        else:
            raise TypeError("Rank subtraction not defined for %s" % type(other))
        
    def __rsub__(self, other):
        if isinstance(other, Rank):
            return (other.num - self.num)
        else:
            raise TypeError("Rank subtraction not defined for %s" % type(other))


class RankList(list):
    """
    Convenience wrapper for a list of ranks, with simplified look-up.
    
    This allows indexes and slices on the ranks to use Rank objects and/or
    string names in addition to integers for accessing the elements of the
    list.
    
    The RankList also has to know what branch it is indexing, so it includes
    a 'branch' attribute.
    """
    def __init__(self, branch, ranks):
        self.branch = branch
        for rank in ranks:
            self.append(rank)
            
    def __getitem__(self, rank):
        if isinstance(rank, Rank):
            i = int(rank)
        elif isinstance(rank, str):
            i = [r.name for r in self].index(rank)
        elif isinstance(rank, int):
            i = rank
        elif isinstance(rank, slice):
            if rank.start is None:
                j = 0
            else:
                j = self.__getitem__(rank.start)
                
            if rank.stop is None:
                k = len(self)
            else:
                k = self.__getitem__(rank.stop)
                
            s = []
            for i in range(j,k):
                s.append(super().__getitem__(i))
                
            return s
        else:
            raise IndexError("Type %s not a valid index for RankList" % type(rank))
        return super().__getitem__(i)
                    
    
# Define the trunk branch object
# This schema will make sense for any unaffiliated Blender document,
# even if it hasn't been saved as a file yet:
trunk = Branch(None, '', 0, ('', 'file', 'scene'))
