# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""A buffer that automatically flushes to disk over a certain size.

This code is not thread-safe, nor is it safe in the presence of
external exceptions."""

import os
import tempfile

DEFAULT_BUFSIZE = 8*1024 # Scientifically proven optimum

class Buffer(object):

    """A buffer that automatically flushes to disk.

    b = Buffer([bufsize]) -- constructor
    len(b) -- number of bytes buffered
    b.write(s) -- appends string s
    b.read() -- reads up to bufsize bytes
    b.rewind() -- resets read position

    All instance variables are considered private.

    """

    def __init__(self, bufsize=DEFAULT_BUFSIZE):
        """Constructor.

        Optional argument:
        bufsize -- in-memory buffer size limit, default 8K

        """
        self.bufsize = bufsize
        self.size = 0
        self.buffer = []
        self.spill = None
        self.pos = 0
        self.reading = False

    def __len__(self):
        """Returns the number of bytes buffered."""
        return self.size

    def write(self, s):
        """Appends a string to the buffer.

        Argument:
        s -- the string to be appended

        """
        newsize = self.size + len(s)
        if self.spill is None:
            if newsize <= self.bufsize:
                # Common case: no spill file yet
                self.buffer.append(s)
                self.size = newsize
                return
            self.spill = tempfile.TemporaryFile(mode="a+b")
            self.spill.write("".join(self.buffer))
            self.buffer = None
        if self.reading:
            self.reading = False
            self.spill.seek(self.size)
        self.spill.write(s)
        self.size = newsize

    def rewind(self):
        """Resets the read position to the beginning of the buffer."""
        self.pos = 0

    def read(self):
        """Returns the next up to bufsize bytes from the buffer.

        This has no arguments.

        This starts where the previous read() left off; initially, or
        after a rewind(), it starts from the beginning.  When the
        buffer is exhausted, return an empty string.

        """
        if self.spill is None:
            if self.pos >= self.size:
                return ""
            assert len(self.buffer) > 0
            if len(self.buffer) > 1:
                self.buffer = ["".join(self.buffer)]
            s = self.buffer[0][self.pos:]
        else:
            self.reading = True
            self.spill.seek(self.pos)
            s = self.spill.read(self.bufsize)
        self.pos += len(s)
        return s

    def readall(self):
        """Reads all remaining data, by calling read() until it runs out."""
        return "".join(iter(self.read, ""))

    def close(self):
        spill = self.spill
        self.spill = self.buffer = self.size = self.pos = None
        if spill is not None:
            spill.close()
