Iterate over SAM file and return SAM entries

    Args:
        handle (file): SAM file handle, can be any iterator so long as it
            it returns subsequent "lines" of a SAM entry

        start_line (str): Next SAM entry, if 'handle' has been partially read
            and you want to start iterating at the next entry, read the next
            SAM entry and pass it to this variable when calling sam_iter.
            See 'Examples.'

        headers (bool): Yields headers if True, else skips lines starting with
            "@"

    Yields:
        SamEntry: class containing all SAM data, yields str for headers if
            headers options is True then yields GamEntry for entries

    Examples:
        The following two examples demonstrate how to use sam_iter.
        Note: These doctests will not pass, examples are only in doctest
        format as per convention. bio_utils uses pytests for testing.

        >>> for entry in sam_iter(open('test.sam')):
        ...     print(entry.qname)  # Print query sequence name
        ...     print(entry.flag)  # Print flag number of alignment
        ...     print(entry.rname)  # Print reference sequence name
        ...     print(entry.pos)  # Print starting position of alignment
        ...     print(entry.mapq)  # Print mapping confidence of alignment
        ...     print(entry.cigar)  # Print CIGAR string of alignment
        ...     print(entry.rnext)  # Print paired read name
        ...     print(entry.pnext)  # Print position of paired read
        ...     print(entry.tlen)  # Print alignment length of all paired reads
        ...     print(entry.seq)  # Print query sequence
        ...     print(entry.qual)  # Print query quality scores
        ...     print(entry.write())  # Print whole SAM entry

        >>> sam_handle = open('test.gff3')
        >>> next(sam_handle)  # Skip first line/entry
        >>> next_line = next(sam_handle)  # Store next entry
        >>> for entry in sam_iter(open('test.sam')):
        ...     print(entry.qname)  # Print query sequence name
        ...     print(entry.flag)  # Print flag number of alignment
        ...     print(entry.rname)  # Print reference sequence name
        ...     print(entry.pos)  # Print starting position of alignment
        ...     print(entry.mapq)  # Print mapping confidence of alignment
        ...     print(entry.cigar)  # Print CIGAR string of alignment
        ...     print(entry.rnext)  # Print paired read name
        ...     print(entry.pnext)  # Print position of paired read
        ...     print(entry.tlen)  # Print alignment length of all paired reads
        ...     print(entry.seq)  # Print query sequence
        ...     print(entry.qual)  # Print query quality scores
        ...     print(entry.write())  # Print whole SAM entry

def sam_iter(handle, start_line=None, headers=False):
    """Iterate over SAM file and return SAM entries

    Args:
        handle (file): SAM file handle, can be any iterator so long as it
            it returns subsequent "lines" of a SAM entry

        start_line (str): Next SAM entry, if 'handle' has been partially read
            and you want to start iterating at the next entry, read the next
            SAM entry and pass it to this variable when calling sam_iter.
            See 'Examples.'

        headers (bool): Yields headers if True, else skips lines starting with
            "@"

    Yields:
        SamEntry: class containing all SAM data, yields str for headers if
            headers options is True then yields GamEntry for entries

    Examples:
        The following two examples demonstrate how to use sam_iter.
        Note: These doctests will not pass, examples are only in doctest
        format as per convention. bio_utils uses pytests for testing.

        >>> for entry in sam_iter(open('test.sam')):
        ...     print(entry.qname)  # Print query sequence name
        ...     print(entry.flag)  # Print flag number of alignment
        ...     print(entry.rname)  # Print reference sequence name
        ...     print(entry.pos)  # Print starting position of alignment
        ...     print(entry.mapq)  # Print mapping confidence of alignment
        ...     print(entry.cigar)  # Print CIGAR string of alignment
        ...     print(entry.rnext)  # Print paired read name
        ...     print(entry.pnext)  # Print position of paired read
        ...     print(entry.tlen)  # Print alignment length of all paired reads
        ...     print(entry.seq)  # Print query sequence
        ...     print(entry.qual)  # Print query quality scores
        ...     print(entry.write())  # Print whole SAM entry

        >>> sam_handle = open('test.gff3')
        >>> next(sam_handle)  # Skip first line/entry
        >>> next_line = next(sam_handle)  # Store next entry
        >>> for entry in sam_iter(open('test.sam')):
        ...     print(entry.qname)  # Print query sequence name
        ...     print(entry.flag)  # Print flag number of alignment
        ...     print(entry.rname)  # Print reference sequence name
        ...     print(entry.pos)  # Print starting position of alignment
        ...     print(entry.mapq)  # Print mapping confidence of alignment
        ...     print(entry.cigar)  # Print CIGAR string of alignment
        ...     print(entry.rnext)  # Print paired read name
        ...     print(entry.pnext)  # Print position of paired read
        ...     print(entry.tlen)  # Print alignment length of all paired reads
        ...     print(entry.seq)  # Print query sequence
        ...     print(entry.qual)  # Print query quality scores
        ...     print(entry.write())  # Print whole SAM entry
    """

    # Speed tricks: reduces function calls
    split = str.split
    strip = str.strip

    next_line = next

    if start_line is None:
        line = next_line(handle)  # Read first B6/M8 entry
    else:
        line = start_line  # Set header to given header

    # Check if input is text or bytestream
    if (isinstance(line, bytes)):
        def next_line(i):
            return next(i).decode('utf-8')

        line = strip(line.decode('utf-8'))
    else:
        line = strip(line)


    # A manual 'for' loop isn't needed to read the file properly and quickly,
    # unlike fasta_iter and fastq_iter, but it is necessary begin iterating
    # partway through a file when the user gives a starting line.
    try:  # Manually construct a for loop to improve speed by using 'next'

        while True:  # Loop until StopIteration Exception raised

            split_line = split(line, '\t')

            if line.startswith('@') and not headers:
                line = strip(next_line(handle))
                continue
            elif line.startswith('@') and headers:
                yield line
                line = strip(next_line(handle))
                continue

            data = SamEntry()
            data.qname = split_line[0]
            try:  # Differentiate between int and hex bit flags
                data.flag = int(split_line[1])
            except ValueError:
                data.flag = split_line[1]
            data.rname = split_line[2]
            data.pos = int(split_line[3])
            data.mapq = int(split_line[4])
            data.cigar = split_line[5]
            data.rnext = split_line[6]
            data.pnext = int(split_line[7])
            data.tlen = int(split_line[8])
            data.seq = split_line[9]
            data.qual = split_line[10]

            line = strip(next_line(handle))  # Raises StopIteration at EOF

            yield data

    except StopIteration:  # Yield last SAM entry
        yield data