# Copyright (C) 2003-2005 Robey Pointer <robey@lag.net>
#
# This file is part of paramiko.
#
# Paramiko is free software; you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation; either version 2.1 of the License, or (at your option)
# any later version.
#
# Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA.

"""
BufferedFile.
"""

from cStringIO import StringIO


_FLAG_READ = 0x1
_FLAG_WRITE = 0x2
_FLAG_APPEND = 0x4
_FLAG_BINARY = 0x10
_FLAG_BUFFERED = 0x20
_FLAG_LINE_BUFFERED = 0x40
_FLAG_UNIVERSAL_NEWLINE = 0x80


class BufferedFile (object):
    """
    Reusable base class to implement python-style file buffering around a
    simpler stream.
    """

    _DEFAULT_BUFSIZE = 8192

    SEEK_SET = 0
    SEEK_CUR = 1
    SEEK_END = 2

    def __init__(self):
        self._flags = 0
        self._bufsize = self._DEFAULT_BUFSIZE
        self._wbuffer = StringIO()
        self._rbuffer = ''
        self._at_trailing_cr = False
        self._closed = False
        # pos - position within the file, according to the user
        # realpos - position according the OS
        # (these may be different because we buffer for line reading)
        self._pos = self._realpos = 0

    def __del__(self):
        self.close()
        
    def __iter__(self):
        """
        Returns an iterator that can be used to iterate over the lines in this
        file.  This iterator happens to return the file itself, since a file is
        its own iterator.

        @raise ValueError: if the file is closed.
        
        @return: an interator.
        @rtype: iterator
        """
        if self._closed:
            raise ValueError('I/O operation on closed file')
        return self

    def close(self):
        """
        Close the file.  Future read and write operations will fail.
        """
        self.flush()
        self._closed = True

    def flush(self):
        """
        Write out any data in the write buffer.  This may do nothing if write
        buffering is not turned on.
        """
        self._write_all(self._wbuffer.getvalue())
        self._wbuffer = StringIO()
        return

    def next(self):
        """
        Returns the next line from the input, or raises L{StopIteration} when
        EOF is hit.  Unlike python file objects, it's okay to mix calls to
        C{next} and L{readline}.

        @raise StopIteration: when the end of the file is reached.

        @return: a line read from the file.
        @rtype: str
        """
        line = self.readline()
        if not line:
            raise StopIteration
        return line

    def read(self, size=None):
        """
        Read at most C{size} bytes from the file (less if we hit the end of the
        file first).  If the C{size} argument is negative or omitted, read all
        the remaining data in the file.

        @param size: maximum number of bytes to read.
        @type size: int
        @return: data read from the file, or an empty string if EOF was
            encountered immediately.
        @rtype: str
        """
        if self._closed:
            raise IOError('File is closed')
        if not (self._flags & _FLAG_READ):
            raise IOError('File not open for reading')
        if (size is None) or (size < 0):
            # go for broke
            result = self._rbuffer
            self._rbuffer = ''
            self._pos += len(result)
            while True:
                try:
                    new_data = self._read(self._DEFAULT_BUFSIZE)
                except EOFError:
                    new_data = None
                if (new_data is None) or (len(new_data) == 0):
                    break
                result += new_data
                self._realpos += len(new_data)
                self._pos += len(new_data)
            return result
        if size <= len(self._rbuffer):
            result = self._rbuffer[:size]
            self._rbuffer = self._rbuffer[size:]
            self._pos += len(result)
            return result
        while len(self._rbuffer) < size:
            try:
                new_data = self._read(max(self._bufsize, size - len(self._rbuffer)))
            except EOFError:
                new_data = None
            if (new_data is None) or (len(new_data) == 0):
                break
            self._rbuffer += new_data
            self._realpos += len(new_data)
        result = self._rbuffer[:size]
        self._rbuffer = self._rbuffer[size:]
        self._pos += len(result)
        return result

    def readline(self, size=None):
        """
        Read one entire line from the file.  A trailing newline character is
        kept in the string (but may be absent when a file ends with an
        incomplete line).  If the size argument is present and non-negative, it
        is a maximum byte count (including the trailing newline) and an
        incomplete line may be returned.  An empty string is returned only when
        EOF is encountered immediately.

        @note: Unlike stdio's C{fgets()}, the returned string contains null
        characters (C{'\\0'}) if they occurred in the input.

        @param size: maximum length of returned string.
        @type size: int
        @return: next line of the file, or an empty string if the end of the
            file has been reached.
        @rtype: str
        """
        # it's almost silly how complex this function is.
        if self._closed:
            raise IOError('File is closed')
        if not (self._flags & _FLAG_READ):
            raise IOError('File not open for reading')
        line = self._rbuffer
        while True:
            if self._at_trailing_cr and (self._flags & _FLAG_UNIVERSAL_NEWLINE) and (len(line) > 0):
                # edge case: the newline may be '\r\n' and we may have read
                # only the first '\r' last time.
                if line[0] == '\n':
                    line = line[1:]
                    self._record_newline('\r\n')
                else:
                    self._record_newline('\r')
                self._at_trailing_cr = False
            # check size before looking for a linefeed, in case we already have
            # enough.
            if (size is not None) and (size >= 0):
                if len(line) >= size:
                    # truncate line and return
                    self._rbuffer = line[size:]
                    line = line[:size]
                    self._pos += len(line)
                    return line
                n = size - len(line)
            else:
                n = self._DEFAULT_BUFSIZE
            if ('\n' in line) or ((self._flags & _FLAG_UNIVERSAL_NEWLINE) and ('\r' in line)):
                break
            try:
                new_data = self._read(n)
            except EOFError:
                new_data = None
            if (new_data is None) or (len(new_data) == 0):
                self._rbuffer = ''
                self._pos += len(line)
                return line
            line += new_data
            self._realpos += len(new_data)
        # find the newline
        pos = line.find('\n')
        if self._flags & _FLAG_UNIVERSAL_NEWLINE:
            rpos = line.find('\r')
            if (rpos >= 0) and ((rpos < pos) or (pos < 0)):
                pos = rpos
        xpos = pos + 1
        if (line[pos] == '\r') and (xpos < len(line)) and (line[xpos] == '\n'):
            xpos += 1
        self._rbuffer = line[xpos:]
        lf = line[pos:xpos]
        line = line[:pos] + '\n'
        if (len(self._rbuffer) == 0) and (lf == '\r'):
            # we could read the line up to a '\r' and there could still be a
            # '\n' following that we read next time.  note that and eat it.
            self._at_trailing_cr = True
        else:
            self._record_newline(lf)
        self._pos += len(line)
        return line

    def readlines(self, sizehint=None):
        """
        Read all remaining lines using L{readline} and return them as a list.
        If the optional C{sizehint} argument is present, instead of reading up
        to EOF, whole lines totalling approximately sizehint bytes (possibly
        after rounding up to an internal buffer size) are read.

        @param sizehint: desired maximum number of bytes to read.
        @type sizehint: int
        @return: list of lines read from the file.
        @rtype: list
        """
        lines = []
        bytes = 0
        while 1:
            line = self.readline()
            if len(line) == 0:
                break
            lines.append(line)
            bytes += len(line)
            if (sizehint is not None) and (bytes >= sizehint):
                break
        return lines

    def seek(self, offset, whence=0):
        """
        Set the file's current position, like stdio's C{fseek}.  Not all file
        objects support seeking.

        @note: If a file is opened in append mode (C{'a'} or C{'a+'}), any seek
            operations will be undone at the next write (as the file position
            will move back to the end of the file).
        
        @param offset: position to move to within the file, relative to
            C{whence}.
        @type offset: int
        @param whence: type of movement: 0 = absolute; 1 = relative to the
            current position; 2 = relative to the end of the file.
        @type whence: int

        @raise IOError: if the file doesn't support random access.
        """
        raise IOError('File does not support seeking.')

    def tell(self):
        """
        Return the file's current position.  This may not be accurate or
        useful if the underlying file doesn't support random access, or was
        opened in append mode.

        @return: file position (in bytes).
        @rtype: int
        """
        return self._pos

    def write(self, data):
        """
        Write data to the file.  If write buffering is on (C{bufsize} was
        specified and non-zero), some or all of the data may not actually be
        written yet.  (Use L{flush} or L{close} to force buffered data to be
        written out.)

        @param data: data to write.
        @type data: str
        """
        if self._closed:
            raise IOError('File is closed')
        if not (self._flags & _FLAG_WRITE):
            raise IOError('File not open for writing')
        if not (self._flags & _FLAG_BUFFERED):
            self._write_all(data)
            return
        self._wbuffer.write(data)
        if self._flags & _FLAG_LINE_BUFFERED:
            # only scan the new data for linefeed, to avoid wasting time.
            last_newline_pos = data.rfind('\n')
            if last_newline_pos >= 0:
                wbuf = self._wbuffer.getvalue()
                last_newline_pos += len(wbuf) - len(data)
                self._write_all(wbuf[:last_newline_pos + 1])
                self._wbuffer = StringIO()
                self._wbuffer.write(wbuf[last_newline_pos + 1:])
            return
        # even if we're line buffering, if the buffer has grown past the
        # buffer size, force a flush.
        if self._wbuffer.tell() >= self._bufsize:
            self.flush()
        return

    def writelines(self, sequence):
        """
        Write a sequence of strings to the file.  The sequence can be any
        iterable object producing strings, typically a list of strings.  (The
        name is intended to match L{readlines}; C{writelines} does not add line
        separators.)

        @param sequence: an iterable sequence of strings.
        @type sequence: sequence
        """
        for line in sequence:
            self.write(line)
        return

    def xreadlines(self):
        """
        Identical to C{iter(f)}.  This is a deprecated file interface that
        predates python iterator support.

        @return: an iterator.
        @rtype: iterator
        """
        return self


    ###  overrides...


    def _read(self, size):
        """
        I{(subclass override)}
        Read data from the stream.  Return C{None} or raise C{EOFError} to
        indicate EOF.
        """
        raise EOFError()

    def _write(self, data):
        """
        I{(subclass override)}
        Write data into the stream.
        """
        raise IOError('write not implemented')

    def _get_size(self):
        """
        I{(subclass override)}
        Return the size of the file.  This is called from within L{_set_mode}
        if the file is opened in append mode, so the file position can be
        tracked and L{seek} and L{tell} will work correctly.  If the file is
        a stream that can't be randomly accessed, you don't need to override
        this method,
        """
        return 0


    ###  internals...


    def _set_mode(self, mode='r', bufsize=-1):
        """
        Subclasses call this method to initialize the BufferedFile.
        """
        if bufsize == 1:
            # apparently, line buffering only affects writes.  reads are only
            # buffered if you call readline (directly or indirectly: iterating
            # over a file will indirectly call readline).
            self._flags |= _FLAG_BUFFERED | _FLAG_LINE_BUFFERED
        elif bufsize > 1:
            self._bufsize = bufsize
            self._flags |= _FLAG_BUFFERED
        if ('r' in mode) or ('+' in mode):
            self._flags |= _FLAG_READ
        if ('w' in mode) or ('+' in mode):
            self._flags |= _FLAG_WRITE
        if ('a' in mode):
            self._flags |= _FLAG_WRITE | _FLAG_APPEND
            self._size = self._get_size()
            self._pos = self._realpos = self._size
        if ('b' in mode):
            self._flags |= _FLAG_BINARY
        if ('U' in mode):
            self._flags |= _FLAG_UNIVERSAL_NEWLINE
            # built-in file objects have this attribute to store which kinds of
            # line terminations they've seen:
            # <http://www.python.org/doc/current/lib/built-in-funcs.html>
            self.newlines = None

    def _write_all(self, data):
        # the underlying stream may be something that does partial writes (like
        # a socket).
        while len(data) > 0:
            count = self._write(data)
            data = data[count:]
            if self._flags & _FLAG_APPEND:
                self._size += count
                self._pos = self._realpos = self._size
            else:
                self._pos += count
                self._realpos += count
        return None

    def _record_newline(self, newline):
        # silliness about tracking what kinds of newlines we've seen.
        # i don't understand why it can be None, a string, or a tuple, instead
        # of just always being a tuple, but we'll emulate that behavior anyway.
        if not (self._flags & _FLAG_UNIVERSAL_NEWLINE):
            return
        if self.newlines is None:
            self.newlines = newline
        elif (type(self.newlines) is str) and (self.newlines != newline):
            self.newlines = (self.newlines, newline)
        elif newline not in self.newlines:
            self.newlines += (newline,)
