"""
Semi-random access to bz2 compressed data.
"""

import bisect

from ._seekbzip2 import SeekBzip2


class SeekableBzip2File:
    """
    Filelike object supporting read-only semi-random access to bz2 compressed
    files for which an offset table (bz2t) has been generated by `bzip-table`.
    """

    def __init__(self, filename, table_filename, **kwargs):
        self.filename = filename
        self.table_filename = table_filename
        self.init_table()
        self.init_bz2()
        self.pos = 0
        self.dirty = True
        self.closed = False

    def init_bz2(self):
        self.seek_bz2 = SeekBzip2(self.filename)

    def init_table(self):
        # Position in plaintext file
        self.table_positions = []
        # Position of corresponding block in bz2 file (bits)
        self.table_bz2positions = []
        pos = 0
        for line in open(self.table_filename):
            fields = line.split()
            # Position of the compressed block in the bz2 file
            bz2_pos = int(fields[0])
            # print >> sys.stderr, fields[0], bz2_pos
            # Length of the block when uncompressed
            length = int(fields[1])
            self.table_positions.append(pos)
            self.table_bz2positions.append(bz2_pos)
            old_pos = pos
            pos = pos + length
            assert pos > old_pos
        self.size = pos

    def close(self):
        self.seek_bz2.close()
        self.closed = True

    def fix_dirty(self):
        # Our virtual position in the uncompressed data is out of sync
        # FIXME: If we're moving to a later position that is still in
        # the same block, we could just read and throw out bytes in the
        # compressed stream, less wasteful then backtracking
        chunk, offset = self.get_chunk_and_offset(self.pos)
        # Get the seek position for that chunk and seek to it
        bz2_seek_pos = self.table_bz2positions[chunk]
        # print >>sys.stderr, "bz2 seek pos:", bz2_seek_pos
        self.seek_bz2.seek(bz2_seek_pos)
        # Consume bytes to move to the correct position
        assert len(self.seek_bz2.read(offset)) == offset
        # Update state
        self.dirty = False

    def read(self, sizehint=-1):
        if sizehint < 0:
            chunks = []
            while True:
                val = self._read(1024 * 1024)
                if val:
                    chunks.append(val)
                else:
                    break
            return b"".join(chunks)
        else:
            return self._read(sizehint)

    def _read(self, size):
        if self.dirty:
            self.fix_dirty()
        val = self.seek_bz2.read(size)
        if val is None:
            # EOF
            self.pos = self.size
            val = b""
        else:
            self.pos = self.pos + len(val)
        return val

    def readline(self, size=-1):
        if self.dirty:
            self.fix_dirty()
        val = self.seek_bz2.readline(size)
        if val is None:
            # EOF
            self.pos = self.size
            val = b""
        else:
            self.pos = self.pos + len(val)
        return val

    def tell(self):
        return self.pos

    def get_chunk_and_offset(self, position):
        # Find the chunk that position is in using a binary search
        chunk = bisect.bisect(self.table_positions, position) - 1
        offset = position - self.table_positions[chunk]
        return chunk, offset

    def seek(self, offset, whence=0):
        # Determine absolute target position
        if whence == 0:
            target_pos = offset
        elif whence == 1:
            target_pos = self.pos + offset
        elif whence == 2:
            target_pos = self.size - offset
        else:
            raise Exception("Invalid `whence` argument: %r", whence)
        # Check if this is a noop
        if target_pos == self.pos:
            return
        # Verify it is valid
        assert 0 <= target_pos < self.size, "Attempt to seek outside file"
        # Move the position
        self.pos = target_pos
        # Mark as dirty, the next time a read is done we need to actually
        # move the position in the bzip2 file
        self.dirty = True

    # ---- File like methods ------------------------------------------------

    def __next__(self):
        ln = self.readline()
        if ln == b"":
            raise StopIteration()
        return ln

    def __iter__(self):
        return self

    def flush(self):
        pass

    def readable(self):
        return True

    def readlines(self, sizehint=-1):
        return [ln for ln in self]

    def seekable(self):
        return True

    def xreadlines(self):
        return iter(self)

    def writable(self):
        return False
