# -*- 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 the base stream class, from which all other streams in
#  arena are derived.  It also defines an interface which is adopted by those
#  classes that are able to stream their data to and from an arbitrary stream.


from Library.Base.Classwork import AnObject

## Used to read and write the length specifier for a string
sStringLengthSpecifier = "=Q"

## Used to read strings
sStreamingString = "={numChars}c"

## This class defines the methods common to all stream objects used by arena.
class AStream(AnObject):
    
    ## Construct a new stream instance.
    def __init__(Self):
        AnObject.__init__(Self)

    ## Initialize the stream instance.        
    def init(Self):        
        return AnObject.init(Self)
    
    ## Read one or more values from the stream.
    #
    #  The values are unpacked from the stream used by the specified format,
    #  which must match the specifications required by \p struct.unpack().  Each
    #  value read from the stream is assigned to a dictionary entry that has
    #  as its key the next unassigned name specified by \p values.
    #
    #  This method should be overridden by descendant classes, which will 
    #  perform the work of reading and unpacking the values.  In the base
    #  implementation, the method does nothing and returns a dictionary with a 
    #  single entry: <tt>{"result": 0}</tt>.
    #
    #  @return Descendant classes should override this method to perform the
    #  act of reading from the stream and to return the values named.  A
    #  dictionary object should be returned that has, as its keys, the names
    #  of the values passed to the function in \p values and one additional
    #  item with a key of "result" that indicates how much information was 
    #  actually read from the stream (i.e., how many bytes were read).
    def readFormat(Self, fmt, *values):        
        return {"result": 0}
    
    ## Write one or more \p values to the stream.
    #
    #  The values are packed into the stream using the specified format, which
    #  must match the specifications required by \p struct.pack().
    #  Alternatively, \p values may be omitted, in which case the routine 
    #  assumes that \p fmt represents a string (perhaps one already packed) 
    #  that is to be written to the stream.
    #
    #  Descendant classes should override this method to perform the act of 
    #  packing the values and writing them to the stream.  The base 
    #  implementation of this method does nothing and returns zero (0).
    #
    #  @return The return value of this method should indicate how much 
    #  information was actually written to the stream (i.e., how many bytes
    #  were written).    
    def writeFormat(Self, fmt, *values):
        return 0    
    
    ## Read one or more \p values from the stream.
    #
    #  This method is provided for convenience, so that values may be read from
    #  a stream without the need to specify a format string for
    #  \p struct.unpack().
    #
    #  This method constructs the necessary format string by testing the type of
    #  each item in \p values.  Boolean values are assumed to be stored as 
    #  such within the stream; anything of type int() or long() is assumed to
    #  be stored as a signed 64-bit integer value; anything of type float() is
    #  assumed to be stored as a 64-bit floating-point value; all other values
    #  are assumed to be stored as strings of varying length.  Once the format 
    #  string has been constructed, this method calls AStream.readFormat().
    #
    #  This method complements AStream.write().  
    def read(Self, **values):        
        valueTypes = ""
        result = {"result": 0}
        
        for value in values.itervalues():
            valueTypes += "="
            if isinstance(value, bool):
                valueTypes += "?"
            
            # A 64-bit signed integer value is assumed for integers
            elif isinstance(value, integer):
                valueTypes += "q"
                
            elif isinstance(value, long):
                valueTypes += "q"
                
            # A 64-bit floating-point value is assumed for floats    
            elif isinstance(value, float):
                valueTypes += "d"
            
            # A string is assumed    
            else:
                if len(value) > 1:
                    valueTypes += str(len(value)) + "s"
                    
                else:
                    valueTypes += "c"
        
        if len(valueTypes) > 0:
            result = Self.readFormat(valueTypes, *values.keys())
            
        return result
    
    ## Write the specified \p values to the stream.
    #
    #  This routine is provided for convenience, so that it need not be 
    #  necessary to construct a format string in order to write to the stream.
    # 
    #  The routine constructs a format string by testing the type of each item
    #  in \p values: boolean values will be stored as such in the stream;
    #  anything of type int() or long() will be stored as a 64-bit signed 
    #  integer value; items of type float() will be stored as 64-bit 
    #  floating-point values; all other values will be stored as strings of 
    #  varying length.  Once the format string has been constructed, this method 
    #  calls AStream.writeFormat(). 
    #
    #  This method complements AStream.read().   
    def write(Self, *values):        
        result = 0
        valueTypes = ""
        
        for value in values:
            valueTypes += "="
            
            if isinstance(value, bool):
                valueTypes += "?"
            
            # A 64-bit signed integer value is assumed for integers
            elif isinstance(value, int):
                valueTypes += "q"
                
            elif isinstance(value, long):
                valueTypes += "q"
                
            # A 64-bit floating-point value is assumed for floats    
            elif isinstance(value, float):
                valueTypes += "d"
            
            # A string is assumed    
            else:
                valueTypes += str(len(value)) + "s"
        
        if len(valueTypes) > 0:
            result = Self.writeFormat(valueTypes, *values)
            
        return result
            
    ## Write a string to the stream, including a length specifier.
    #    
    #  Strings written by this routine can be recalled from the stream by
    #  a call to AStream.readString().
    #    
    #  @return The return value of this routine indicates how many bytes were 
    #  written to the stream.            
    def writeString(Self, thisString):
        # Store the number of bytes written to the stream
        result = 0
        
        if isinstance(thisString, string):
            stringLength = len(thisString)
            result = Self.writeFormat(sStringLengthSpecifier, stringLength)
            if stringLength > 0:
                result += Self.writeFormat(thisString)
        
        return result

    ## Read a string from the stream.
    #    
    #  This routine reads the length specifier for the string and then the
    #  string value from the stream.  It can be used to recall a string that
    #  was previously written by AStream.writeString().
    #    
    #  @return The return value of this routine is a tuple: the first item
    #  indicates how many bytes were read from the stream; the second item is
    #  the string itself.
    def readString(Self): 
        # Read the length specifier from the stream    
        incoming = Self.readFormat(sStringLengthSpecifier, "length")
        result = incoming["result"]
        text = ""
        
        # Read the string
        if incoming["length"] > 0:
            incoming = sStreamingString.format(numChars=stringLength)
            incoming = Self.readFormat(sStreamingString.format(
              numChars=incoming["length"]), "text")
              
            text = incoming["text"]
            
        return result, text
        
    ## Rewind the stream by the specified number of bytes
    def rewindBy(Self, count):
        if count < 0:
            count *= -1
            
        if count > Self.length():
            count = Self.length()
            
        previousPosition = Self.position()
        Self.moveTo(previousPosition - count)
        
        return previousPosition
        
    # Properties ---------------------------------------------------------------
    ## Return the current position within the stream.
    #    
    #  This routine is called whenever a program attempts to access the
    #  AStream.position attribute.  In the default implementation of AStream,
    #  it always returns zero (0).  Descendant classes should override this
    #  method to produce more relevant output.
    #
    #  The position within a stream is measured as the number of bytes from
    #  the beginning of the stream.  The beginning of the stream is always
    #  located at position zero (0).
    def position(Self):        
        return 0
        
    ## Set the current position within the stream.
    #    
    #  This routine is called whenever a program attempts to modify the
    #  AStream.position attribute.  In the default implementation of AStream,
    #  it does nothing.  Descendant classes should override this method.
    #
    #  The position within a stream is measured as the number of bytes from
    #  the beginning of the stream.  The beginning of the stream is always
    #  located at position zero (0).  If \p newPosition specifies a value that
    #  is less than zero, it is assumed to indicate an offset relative to the
    #  end of the stream (i.e., AStream.length + \p newPosition + 1).  The end
    #  of the stream is always located at position -1.
    def moveTo(Self, newPosition):
        pass
        
    ## Get the current length of the stream, in bytes.
    #    
    #  This routine is called whenever a program attempts to access the 
    #  AStream.length attribute, either directly or by a call to
    #  len().  In the default implementation of AStream, it always
    #  returns zero (0).  Descendant classes should override this method to
    #  produce more meaningful output.
    def length(Self):        
        return 0
        
    ## Get the current length of the stream, in bytes.
    #     
    #  This routine allows len() to be used on instances of AStream and its
    #  descendants.  It simply returns the value of a call to AStream.length().
    def __len__(Self):        
        return Self.length()
        
    ## Determine whether or not the end of the stream has been reached.
    #    
    #  In the base implementation of AStream, this routine simply checks to
    #  see whether AStream.position >= AStream.length.  If so, it returns
    #  \p True; otherwise it returns \p False.  Descendant classes should 
    #  override this method where a better test is available.        
    def hasEnded(Self):        
        return Self.position() >= Self.length()
        
## This class defines the methods which must be implemented by classes that are
#  capable of streaming their instance data to and from an arbitrary stream.
class IsStreamable:
    
    ## Write class instance data to the specified stream.
    #    
    #  The return value of this function should indicate how much information
    #  was actually written to the destination stream (i.e., how many bytes
    #  were written).
    def writeTo(Self, Dest):        
        return 0
            
    ## Read class instance data from the specified stream.
    #   
    #  The return value of this function should indicate how much information
    #  was actually read from the destination stream (i.e., how many bytes
    #  were read). """        
    def readFrom(Self, Source):        
        return 0
