#!/usr/bin/env python
# -*- coding: UTF-8 -*-
#    This file is part of arena.
#
#    arena is free software: you can redistribute it and/or modify it
#    under the terms of the GNU General Public License as published by the
#    Free Software Foundation; either version 3 of the License or, at your
#    option, any later version.
#
#    arena is distributed in the hope that it will be useful, but
#    WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABLITY
#    or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
#    License for more details.
#
#    You should have received a copy of the GNU General Public License along
#    with arena.  If not, see <http://www.gnu.org/licenses/>.

## @file
#  This file defines and implements a class that manages a binary tree and
#  a class that represents a generic node in that tree.

from Library.Base.Classwork import AnObject
from Library.IO.Streams import IsStreamable
from Library.String.Utilities import String, IsPrintable
from UserDict import DictMixin

## The format used when creating a string representation of ABinaryLeaf
blStringRepresentation = "\t{name} at index {index} with key: {key}"
## The format used when creating a string representation of ABinaryTree
btStringRepresentationSingular = "{name} with {count} item"
btStringRepresentationPlural = "{name} with {count} items"

## The error message used when an attempt is made to retrieve a non-existent
#  item from a binary tree
btErrorRetrievingItem = ("Failed to retrieve the item \"{itemname}\""
                         " from {classname}")

## A node in a binary tree.
#    
#  While this class may seem redundant where Python dictionary objects are
#  available, it allows us to standardize the way items are stored in that 
#  dictionary, which is useful for something like a language parser.
class ABinaryLeaf(AnObject, IsPrintable, IsStreamable):
    ## Constructor    
    def __init__(Self, thisKey):
        AnObject.__init__(Self)
        
        Self._key = thisKey
        
    ## Initializer
    def init(Self):
        result = AnObject.init(Self)
        
        if result:
            # Establish private attributes
            # The key used to sort the node
            Self._key = 0
            # Establish public attributes
            # The index of the node within its tree
            Self.index = 0
            
        return result

    # IsStreamable implementation ----------------------------------------------
    ## Writes the node and its associated subtrees to the stream.
    #    
    #  The total number of bytes written to the stream is returned.
    def writeTo(Self, Dest):
        result = 0
        
        if Dest is not None:
            # Write our index first
            result = Dest.write(Self.index)
            # Write our key next
            result += Dest.write(Self._key)
                
        return result
        
    ## Read an individual node from the specified stream.
    #    
    #  This method is designed to do the reverse of what the streamTo()
    #  method does.  It reads an individaul node from the specified stream
    #  and returns the total number of bytes read.
    def readFrom(Self, Source):
        result = 0
        
        if Source is not None:
            # Read the index first
            result = Source.read(Self.index)
            # Read the key next
            key = Self._key
            result += Source.read(key)
            Self._key = key
            
        return result
        
    # IsPrintable implementation -----------------------------------------------
    ## Construct a string representation of the node.
    #    
    #  The representation uses the format specified by #blStringRepresentation.
    def toString(Self):
        return blStringRepresentation.format(name=Self.name(), 
          index=Self.index, key=Self._key)
          
    # Properties ---------------------------------------------------------------
    ## Retrieve the key used to sort the node.
    def key(Self):
        return Self._key
      
## A binary tree.        
class ABinaryTree(AnObject, DictMixin, IsPrintable, IsStreamable):
    ## Constructor
    def __init__(Self):
        AnObject.__init__(Self)

    ## Initializer
    def init(Self):
        result = AnObject.init(Self)

        if result == True:
            # Establish private attributes
            ## The items in the tree
            Self._items = {}
            ## The number of items in the tree
            Self._census = 0

        return result
    
    ## Destructor    
    def __del__(Self):
        del Self._items
            
        AnObject.__del__(Self)

    ## Fetch the node with the specified key from the tree.
    #
    #  If a node that has \p thisKey is found within the tree, a reference to
    #  it is returned; otherwise, this routine returns \p None.
    def fetch(Self, thisKey):
        result = None
        
        if thisKey in Self._items:
            result = Self._items[thisKey]
            
        return result
     
    ## Determine whether or not the specified key exists within the tree.
    def has(Self, thisKey):
        return thisKey in Self._items

   ## Insert the specified item into the tree.
   #
   #  If \p ThisItem has a key that already exists within the tree, this 
   #  routine will return \p False and no insertion will take place (in other
   #  words, duplicate sort keys are not allowed).  Otherwise, the routine
   #  inserts the node and returns \p True.
    def insert(Self, ThisItem):
        result = False
        
        if ThisItem is not None:
            if ThisItem.key() not in Self._items:
                ThisItem.index = Self._census
                Self._items[ThisItem.key()] = ThisItem
                Self._census += 1
                result = True
                
        return result

    ## Delete the specified item from the tree.
    #
    #  If a node with \p thisKey is found within the tree, it is removed from
    #  the tree and discarded.  The left and right subtrees of the affected node
    #  are re-inserted into the tree.  However, the index values of each node
    #  are not updated; this means that a tree from which nodes have been 
    #  removed may show gaps when it is serialized.
    def delete(Self, thisKey):
        result = False
            
        if thisKey in Self._items:
            del Self._items[thisKey]
            result = True
            
        return result

    # IsStreamable implementation ----------------------------------------------
    ## Write the tree and its nodes to the specified stream.
    def writeTo(Self, Dest):
        result = 0
        if Dest is not None:
            # Write the number of items in the tree to the specified stream
            result = Dest.write(Self._census)
            
            if len(Self._items) > 0:
                for Item in Self._items.itervalues():
                    result += Item.writeTo(Dest)

    ## Read the tree from the specified stream.  The base implementation of this
    #  routine does nothing; it is meant to be overridden by descendant
    #  classes.
    def readFrom(Self, Source):
        return 0

    # IsPrintable implementation -----------------------------------------------
    ## Return a string representation of the binary tree.
    #
    #  The representation returned is constructed using the format specified
    #  by #btStringRepresentationPlural or #btStringRepresentationSingular,
    #  depending on how many nodes are contained within the tree.
    def toString(Self):
        if Self._census > 0:
            return btStringRepresentationPlural.format(name=Self.displayName(),
              count=Self._census)
              
        else:
          return btStringRepresentationSingular.format(name=Self.displayName(),
            count=Self._census)

    ## Print a string representation of the tree and its nodes to the specified
    #  stream.
    def printTo(Self, Dest):
        result = 0

        if Dest is not None:
            result = Dest.write(Self.toString() + "\n")
            if len(Self._items) > 0:
                for Item in Self._items.itervalues():
                    result += Item.printTo(Dest)
                
        return result

    # Properties ---------------------------------------------------------------
    ## Retrieve the number of items in the tree.
    def census(Self):
        return Self._census
     
    ## Retrieve a list of keys for the item in the tree.
    def keys(Self):
        return Self._items.keys()

    ## Return the number of items in the tree.  This method allows the \p len()
    #  function to be used on an instance of the tree.
    def __len__(Self):
        return Self._census

    ## Get value of the specified item from the binary tree.
    #
    #  This routine is provided for convenience, so that access to the items
    #  in the tree can be programmed as though it was a native Python 
    #  dictionary object.
    def __getitem__(Self, key):
        Result = None

        if key in Self._items:
            Result = Self._items[key]
            
        if Result is None:
            raise KeyError(btErrorRetrievingItem.format(itemname=key,
              classname=Self.displayName()))
              
        return Result

    ## Delete the specified field.
    #
    #  This routine is provided for convenience, so that access to the items in
    #  the tree can be programmed as though the tree was a native Python 
    #  dictionary object.
    def __delitem__(Self, key):
        Self.delete(key)

    ## Determine whether or not the specified key exists in the binary
    #  tree.
    #
    #  This routine is provided for convenience, so that access to the items in
    #  the binary tree can be programmed as though the tree was a native 
    #  Python dictionary object.
    def __contains__(Self, key):
        return Self.has(key)
        
    ## Iterate over the items in the tree.  This method allows an instance of
    #  of a binary tree to be used as part of a <tt>for..in</tt> statement.
    def __iter__(Self):
        return Self._items.itervalues()
