# -*- 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 classes for interacting with file streams containing 
#   binary data.  The stream data is packed and unpacked using routines from
#   the \p struct library.

from Library.IO.Streams import AStream
from struct import pack, unpack, Struct
import io, os.path

## Represents an error that can occur when reading from a stream
fisReadError = "an error occurred while reading from \"{filename}\""

## Represents an error that can occur when writing to a stream
fisWriteError = "an error occurred while writing to \"{filename}\""

## The base file stream class, from which all others are derived.
#    
#  This class does not implement the read() and write() methods it inherites
#  from AStream.  Consequently, this class should never be instantiated 
#  directly. 
class AFileStream(AStream):
    
    ## Initialize a new file stream instance.
    #    
    #    This routine clears the attributes of the class.
    def init(Self):        
        result = AStream.init(Self)
        
        if result == True:
            # The name of the file being streamed
            Self._fileName = ""
            # The handle of the file being streamed (a stream itself, in this 
            # case)
            Self._Handle = None
        
        return result
        
    ## Close the file and flush any remaining data to disk.
    #    
    #  This routine is called automatically by the destructor for AFileStream;
    #  however, there may be instances when it is desirable to explicity call
    #  this method (such as when finished writing to a log file), since Python's
    #  reference counting scheme does not guarantee that the destructor will be
    #  called at a specific time.
    #
    #  Descendant classes that override this method should gracefully handle
    #  multiple calls to this routine.
    def close(Self):        
        if isinstance(Self._Handle, io.IOBase):
          Self._Handle.close()
          Self._Handle = None
          
    ## Destroy the file stream instance.
    #    
    #  This routine calls AFileStream.close() to ensure that the stream is
    #  closed and any remaining data is flushed to the disk before the instance
    #  is destroyed.
          
    def __del__(Self):        
        Self.close()
        
    # IsPrintable implementation -----------------------------------------------
    ## Return a string representation of the file stream.
    #
    #  The representation returned is the name of the file being streamed.
    def toString(Self):        
        return Self._fileName
        
    # Properties ---------------------------------------------------------------
    ## Return the current position within the stream.
    #
    #  The current position within the stream is measured as the number of
    #  bytes from the beginning of the stream, which is always located at
    #  position zero (0).
    def position(Self):        
        if isinstance(Self._Handle, io.IOBase):
            if Self._Handle.seekable():
                return Self._Handle.tell()
            
        return 0
        
    ## Set the current position within the stream.
    #
    #  The current position within the stream is measured as the number of
    #  bytes from the beginning of the stream, which is always located at
    #  position zero (0).  If \p newPosition specifies an offset that is less
    #  than zero, it is assumed to indicate a position within the stream 
    #  relative to the end of the stream (i.e., AFileStream.length + \p 
    #  newPosition + 1).  The end of the stream is always located at
    #  offset -1.       
    def moveTo(Self, newPosition):        
        if isinstance(Self._Handle, io.IOBase):
            if (Self._Handle.seekable()):
                if newPosition < 0:
                    newPosition += Self.length + 1
                    
                if newPosition < 0:
                    newPosition = 0
            
                Self._Handle.seek(newPosition)
      
    ## Return the current length of the stream, in bytes.
    #
    #  Depending on the file being streamed, it can be time-consuming to use 
    #  this method, as it must save the current position within the stream, 
    #  traverse the stream to the end of the file, determine what offset that 
    #  is, then return to the previous position within the stream.  This method
    #  should therefore be used sparingly.
    def length(Self):        
        result = 0
        if isinstance(Self._Handle, io.IOBase):
            if Self._Handle.seekable():
                previousPosition = Self._Handle.tell()
                # Seek the end of the stream
                Self._Handle.seek(0, 2)
                result = Self._Handle.tell()
                # Return to the previous position
                Self._Handle.seek(previousPosition)
                
    ## Retrieve the name of the file being streamed.                     
    def fileName(Self):        
        return Self._fileName
                
## This class represents a stream that is used to read binary data from a file.
class AFileInputStream(AFileStream):
    
    ## Construct a new file stream instance and open \p fileName for reading.
    #
    #  If \p fileName does not exist, it is created.
    def __init__(Self, fileName):
        AFileStream.__init__(Self)
        Self._fileName = fileName
        if len(Self._fileName) > 0:
            Self._Handle = io.open(Self._fileName, mode="rb", 
              buffering=io.DEFAULT_BUFFER_SIZE)
            
    ## Initialize the new file stream instance.        
    #
    #  This routine sets up a new private attribute named "length" that is
    #  returned by AFileInputStream.length.  This is more efficient than
    #  repeatedly seeking the end of the stream for each time AStream.length is
    #  queried, which is what would occur if the default implementation 
    #  inherited from AFileStream was left in place.
    def init(Self):        
        if AFileStream.init(Self) == True:
            Self._length = -1    
        
    ## Read each item named in \p values from the stream.
    #
    #  \return This method returns a dictionary object that associates each
    #  item named in \p values with a value read from the stream.  The 
    #  dictionary object also includes an item named \p "result" that indicates 
    #  how much data was read from the stream.
    #
    #  If an error occurs while reading, an exception is thrown.
    def readFormat(Self, fmt, *values):
        result = {"result": 0}
        
        if (isinstance(Self._Handle, io.IOBase)) and (len(fmt) > 0):
            # Compile the value types
            ValueTypes = Struct(fmt)
            # Read the values
            rawValues = Self._Handle.read(ValueTypes.size)
            result["result"] = len(rawValues)
            
            # The resulting data should be the size expected
            if result["result"] == ValueTypes.size:
                # Decode the values
                rawValues = ValueTypes.unpack(rawValues)
                
                for i in xrange(0, len(rawValues)):
                    result[values[i]] = rawValues[i]
                    
            # If it is greater than zero, SOME data was retrieved, but not
            # as much as was expected                            
            elif result > 0:
                raise AnException(fisReadError, filename=Self._fileName)
            
        return result
        
    # Properties ---------------------------------------------------------------
    ## Get the current size of the stream.
    #
    #  This routine overriddes the implementation inherited from AFileStream in
    #  order to provide one that is more efficient.  Since AFileInputStream is
    #  read-only, its size will never change, and so the end of the stream is
    #  sought only once to determine its size.  Subsequent calls to this 
    #  method retrieve the stored size rather than causing another search for
    #  the end of the stream.
    def length(Self):        
        # Return the stored value, if one has been set
        if Self._length != -1:
            return Self._length
            
        # Determine the length of the stream this one time
        else:
            Self._length = AFileStream.length(Self)
            return Self._length
 
## This class represents a stream that is used to write binary information to
#  a file.                    
class AFileOutputStream(AFileStream):
    
    ## Construct a new file output stream instance and open \p fileName for
    #  writing.  
    #
    #  If \p fileName does not exist, then it will be created. If \p replace
    #  is \p True and the file exists, then it will be truncated when it is
    #  opened, and any data previously contained within the file will be lost.
    #  Otherwise, the file is opened for appending.
    def __init__(Self, fileName, replace=False):
        
        AFileStream.__init__(Self)
        
        Self._fileName = fileName
        if len(Self._fileName) > 0:
            if (replace) or (os.path.exists(Self._fileName) == False):
                Self._Handle = io.open(Self._fileName, mode="wb", 
                buffering=0)
                
            else:
                Self._Handle = io.open(Self._fileName, mode="ab", 
                  buffering=0)
                
    ## Write the specified \p values to the stream.
    #
    #  If \p values is empty, then \p fmt is written to the stream.
    #  If an error occurs while writing, an exception will be raised.
    # 
    #  \return This method returns a value that indicates how many bytes were
    #  written to the stream.
    def writeFormat(Self, fmt, *values):
        
        result = 0
        if (Self._Handle is not None) and (len(fmt) > 0):
            # If a format string and values were provided....
            if len(values) > 0:
                # Compile the value types
                ValueTypes = Struct(fmt)
                # Write the values
                rawValues = ValueTypes.pack(*values)
                result = Self._Handle.write(rawValues) 
            
                # The resulting data should be the size expected
                if result != ValueTypes.size:
                    raise AnException(fisWriteError, filename=Self._fileName)
                    
            # A single string was provided
            else:
                result = Self._Handle.write(fmt)
          
        return result
