Generate reads from ambiguous alignment file

    Extract reads found to be mapping an input FASTA bin.
    If one read maps, the whole pair is extracted and written
    to the output paired-end FASTQ files. Reads that mapped
    and weren't part of a pair are kept in a third 'single'
    file for people who need it (e.g. to get extra paired reads
    by fetching the opposite one from the original FASTQ library).

    Parameters
    ----------
    sam_merged : file, str or pathlib.Path
        The input alignment file in SAM/BAM format to be processed.
    output_dir : str or pathlib.Path
        The output directory to write the network and chunk data into.
    parameters : dict, optional
        Parameters for the network to read conversion, similar to
        alignment_to_network.
    save_memory : bool, optional
        Whether to keep the read names into memory or write them in different
        files, which takes longer but may prevent out-of-memory crashes.
        Default is True.
    `*bin_fasta` : file, str or pathlib.Path
        The bin FASTA files with appropriately named records.

    Returns
    -------
    A dictionary of files with read names for each bin if save_memory is True,
    and a dictionary of the read names lists themselves otherwise.


    Note
    ----
    This will throw an IOError ('close failed in file object destructor') on
    exit with older versions of pysam for some reason. It's harmless but
    you may consider upgrading to a later version of pysam if it comes up in
    a pipeline.

def alignment_to_reads(
    sam_merged,
    output_dir,
    parameters=DEFAULT_PARAMETERS,
    save_memory=True,
    *bin_fasta
):
    """Generate reads from ambiguous alignment file

    Extract reads found to be mapping an input FASTA bin.
    If one read maps, the whole pair is extracted and written
    to the output paired-end FASTQ files. Reads that mapped
    and weren't part of a pair are kept in a third 'single'
    file for people who need it (e.g. to get extra paired reads
    by fetching the opposite one from the original FASTQ library).

    Parameters
    ----------
    sam_merged : file, str or pathlib.Path
        The input alignment file in SAM/BAM format to be processed.
    output_dir : str or pathlib.Path
        The output directory to write the network and chunk data into.
    parameters : dict, optional
        Parameters for the network to read conversion, similar to
        alignment_to_network.
    save_memory : bool, optional
        Whether to keep the read names into memory or write them in different
        files, which takes longer but may prevent out-of-memory crashes.
        Default is True.
    `*bin_fasta` : file, str or pathlib.Path
        The bin FASTA files with appropriately named records.

    Returns
    -------
    A dictionary of files with read names for each bin if save_memory is True,
    and a dictionary of the read names lists themselves otherwise.


    Note
    ----
    This will throw an IOError ('close failed in file object destructor') on
    exit with older versions of pysam for some reason. It's harmless but
    you may consider upgrading to a later version of pysam if it comes up in
    a pipeline.
    """

    #   Just in case file objects are sent as input
    def get_file_string(file_thing):
        try:
            file_string = file_thing.name
        except AttributeError:
            file_string = str(file_thing)
        return file_string

    #   Global set of chunks against which reads are required to
    #   map - we store them in a tuple that keeps track of the
    #   original bin each chunk came from so we can reattribute the reads later

    bin_chunks = set()
    for bin_file in bin_fasta:
        for record in SeqIO.parse(bin_file, "fasta"):
            bin_chunks.add((get_file_string(bin_file), record.id))

    chunk_size = int(parameters["chunk_size"])

    mapq_threshold = int(parameters["mapq_threshold"])

    def read_name(read):
        return read.query_name.split()[0]

    #   Since reading a huge BAM file can take up a
    #   lot of time and resources, we only do it once
    #   but that requires opening fastq files for writing
    #   as matching reads get detected along the
    #   bam and keeping track of which ones are
    #   currently open.

    def get_base_name(bin_file):

        base_name = ".".join(os.path.basename(bin_file).split(".")[:-1])

        output_path = os.path.join(
            output_dir, "{}.readnames".format(base_name)
        )

        return output_path

    if save_memory:
        opened_files = dict()
    else:
        read_names = dict()

    with pysam.AlignmentFile(sam_merged, "rb") as alignment_merged_handle:

        for (my_read_name, alignment_pool) in itertools.groupby(
            alignment_merged_handle, read_name
        ):

            for my_alignment in alignment_pool:

                relative_position = my_alignment.reference_start
                contig_name = my_alignment.reference_name

                chunk_position = relative_position // chunk_size

                # The 'chunk name' is used to detect macthing positions
                chunk_name = "{}_{}".format(contig_name, chunk_position)

                # But such matching positions have to map acceptably
                quality_test = my_alignment.mapping_quality > mapq_threshold

                for bin_file in bin_fasta:
                    chunk_tuple = (bin_file, chunk_name)
                    if chunk_tuple in bin_chunks and quality_test:
                        if save_memory:
                            output_path = get_base_name(bin_file)
                            try:
                                output_handle = opened_files[bin_file]
                            except KeyError:
                                output_handle = open(output_path, "w")
                                opened_files[bin_file] = output_handle

                            output_handle.write("@{}\n".format(my_read_name))

                        else:
                            try:
                                read_names[my_read_name].append(bin_file)
                            except KeyError:
                                read_names[my_read_name] = [bin_file]

    for file_handle in opened_files.values():
        file_handle.close()
    #   Return unpaired file names for pair_unpaired_reads() to process
    if save_memory:
        return opened_files.keys()
    else:
        return read_names