'''Blat.py - tools for working with PSL formatted files and data
=============================================================

IMPORTANT NOTICE:
Several functions within this module are scheduled for
deprecation in May 2024.
Details regarding the specific functions to be deprecated
can be found in the documentation of each function.


This module provides a class to parse :term:`PSL` formatted
files such as those output by the BLAT tool.

This module defines the :class:`Blat.Match` class representing a
single entry and a series of iterators to iterate of :term:`SL`
formatted files (:func:`iterator`, :func:`iterator_target_overlap`,
...).

Reference
---------

'''
import copy
import string
import collections
import warnings
import functools

try:
    import alignlib_lite
except ImportError:
    pass

from cgat import Components as Components
from cgatcore import experiment as E


def deprecated_class(cls):
    orig_init = cls.__init__

    @functools.wraps(orig_init)
    def new_init(self, *args, **kwargs):
        warnings.warn(f"{cls.__name__} is deprecated and will be removed in May 2024.", DeprecationWarning)
        orig_init(self, *args, **kwargs)

    cls.__init__ = new_init
    return cls


@deprecated_class
class Error(Exception):
    """Base class for exceptions in this module."""

    def __str__(self):
        return str(self._message)

    def _get_message(self):
        return self._message

    def _set_message(self, message):
        self._message = message
    message = property(_get_message, _set_message)


@deprecated_class
class ParsingError(Error):

    """Exception raised for errors while parsing

    Attributes:
        message -- explanation of the error
    """

    def __init__(self, message, line=None):
        if line:
            self.message = message + " at line:" + line
        else:
            self.message = message

HEADER = """psLayout version 3

match   mis-    rep.    N's     Q gap   Q gap   T gap   T gap   strand  Q       Q       Q       Q       T       T       T       T       block   blockSizes      qStarts  tStarts
        match                   count   bases   count   bases           name    size    start   end     name    size    start   end     count
---------------------------------------------------------------------------------------------------------------------------------------------------------------"""


@deprecated_class
class Match:

    """a :term:`psl` formatted alignment.

    Block coordinates are on the forward strand for target and on
    the forward/reverse strand for the query depending on the strand.

    The fields mQueryFrom/To and mSbjctFrom/To are always on the forward
    strand.
    """

    def __init__(self):
        self.mNMatches = 0
        self.mNMismatches = 0
        self.mNRepMatches = 0
        self.mNns = 0
        self.mQueryNGapsCounts = 0
        self.mQueryNGapsBases = 0
        self.mSbjctNGapsCounts = 0
        self.mSbjctNGapsBases = 0
        self.strand = "+"
        self.mQueryId = ""
        self.mQueryLength = 0
        self.mQueryFrom = 0
        self.mQueryTo = 0
        self.mSbjctId = ""
        self.mSbjctLength = 0
        self.mSbjctFrom = 0
        self.mSbjctTo = 0
        self.mNBlocks = 0
        self.mBlockSizes = []
        self.mQueryBlockStarts = []
        self.mSbjctBlockStarts = []

    def convertCoordinates(self):
        """convert coordinates.

        This rescales the block positions so that they start at 0 and converts
        the query to forward and the sbjct to forward/reverse coordinates.

        About the psl psl format from the manual at
        http://genome.ucsc.edu/google/goldenPath/help/pslSpec.html

        ::
           In general the coordinates in psl files are "zero based
           half open." The first base in a sequence is numbered zero
           rather than one.  When representing a range the end
           coordinate is not included in the range. Thus the first 100
           bases of a sequence are represented as 0-100, and the
           second 100 bases are represented as 100-200.

           There is a another little unusual feature in the .psl
           format. It has to do with how coordinates are handled on
           the negative strand.  In the qStart/qEnd fields the
           coordinates are where it matches from the point of view of
           the forward strand (even when the match is on the reverse
           strand). However on the qStarts[] list, the coordinates are
           reversed.

        This class works in forward coordinates for the query and
        forward/reverse coordinates for the sbjct.

        For a negative strand match, the following is done:
           * invert mSbjctFrom and mSbjctTo with mSbjctLength
           * add block sizes to mQueryStarts and mSbjctStarts
           * invert mQueryStarts and mSbjctStarts
           * reverse blocksize, mQueryStarts and mSbjctStarts

        """

        block_sizes = self.mBlockSize
        query_starts = self.mQueryBlockStarts
        sbjct_starts = self.mSbjctBlockStarts

        if self.mSbjctStrand == "-":
            self.mSbjctFrom, self.mSbjctTo = self.mSbjctLength - \
                self.mSbjctTo, self.mSbjctLength - self.mSbjctFrom
            query_starts = [
                self.mQueryLength - x - y for x, y in zip(query_starts, block_sizes)]
            sbjct_starts = [
                self.mSbjctLength - x - y for x, y in zip(sbjct_starts, block_sizes)]
            query_starts.reverse()
            sbjct_starts.reverse()

        self.mQueryStarts = [x - self.mQueryFrom for x in query_starts]
        self.mSbjctStarts = [x - self.mSbjctFrom for x in sbjct_starts]

    def _switchQueryStrandFlag(self):
        '''switch the strand flag of the query
        '''

        if self.strand == "-":
            self.strand = "+"
        else:
            self.strand = "-"

    def switchTargetStrand(self):
        """switch the target strand.

        Use in cases in which a feature has been defined on the
        negative target strand with reverse coordinates. The result
        will be the same alignment using forward coordinates on the
        target.

        This method will also update the query strand and coordinates.
        """

        block_sizes = self.mBlockSizes
        query_starts = self.mQueryBlockStarts
        sbjct_starts = self.mSbjctBlockStarts

        # invert alignment
        self.mQueryBlockStarts = [
            self.mQueryLength - x - y
            for x, y in zip(query_starts, block_sizes)]
        self.mQueryBlockStarts.reverse()
        self.mSbjctBlockStarts = [
            self.mSbjctLength - x - y
            for x, y in zip(sbjct_starts, block_sizes)]
        self.mSbjctBlockStarts.reverse()
        self.mBlockSizes.reverse()

        # update target coordinates only
        self.mSbjctFrom, self.mSbjctTo = self.mSbjctLength - \
            self.mSbjctTo, self.mSbjctLength - self.mSbjctFrom
        self._switchQueryStrandFlag()

    def fromTable(self, data):

        try:
            (nmatches, nmismatches, nrepmatches, nns,
             query_ngaps_counts, query_ngaps_bases,
             sbjct_ngaps_counts, sbjct_ngaps_bases,
             strand,
             query_id, query_length, query_from, query_to,
             sbjct_id, sbjct_length, sbjct_from, sbjct_to,
             nblocks, block_sizes,
             query_block_starts, sbjct_block_starts) = data[:21]
        except ValueError:
            raise ParsingError("parsing error: %i fields" %
                               len(data), "\t".join(data))

        nmatches, nmismatches, nrepmatches, nns = list(map(
            int,
            (nmatches, nmismatches, nrepmatches, nns)))

        self.mNMatches = nmatches
        self.mNMismatches = nmismatches
        self.mNRepMatches = nrepmatches
        self.mNns = nns
        self.mQueryNGapsCounts = int(query_ngaps_counts)
        self.mQueryNGapsBases = int(query_ngaps_bases)
        self.mSbjctNGapsCounts = int(sbjct_ngaps_counts)
        self.mSbjctNGapsBases = int(sbjct_ngaps_bases)
        self.strand = strand
        self.mQueryId = query_id
        self.mQueryLength = int(query_length)
        self.mQueryFrom = int(query_from)
        self.mQueryTo = int(query_to)
        self.mSbjctId = sbjct_id
        self.mSbjctLength = int(sbjct_length)
        self.mSbjctFrom = int(sbjct_from)
        self.mSbjctTo = int(sbjct_to)
        self.mNBlocks = int(nblocks)
        self.mBlockSizes = list(map(int, block_sizes[:-1].split(",")))
        self.mQueryBlockStarts = list(
            map(int, query_block_starts[:-1].split(",")))
        self.mSbjctBlockStarts = list(
            map(int, sbjct_block_starts[:-1].split(",")))

        # this makes sure that the block positions are rescaled
        if self.mQueryLength != 0:
            self.mQueryCoverage = 100.0 * \
                (self.mNMismatches + self.mNMatches) / self.mQueryLength
        else:
            self.mQueryCoverage = 0

        if self.mSbjctLength != 0:
            self.mSbjctCoverage = 100.0 * \
                (self.mNMismatches + self.mNMatches) / self.mSbjctLength
        else:
            self.mSbjctCoverage = 0

        if nmatches + nmismatches > 0:
            self.mPid = 100.0 * float(nmatches) / (nmatches + nmismatches)
        else:
            self.mPid = 100.0

    def fromMaq(self, maq):
        """build BLAT entry from a MAQ match.

        see :class:`Maq.Match`.
        """
        start = maq.start - 1
        l = maq.mLength
        self.mQueryId = maq.mName
        self.mSbjctId = maq.contig
        self.mQueryFrom = 0
        self.mQueryTo = l
        self.mQueryLength = l
        self.strand = maq.strand
        self.mSbjctFrom = start
        self.mSbjctTo = start + l
        self.mNMismatches = maq.mNMismatches
        self.mNMatches = l - self.mNMismatches
        self.mBlockSizes = [l]
        self.mQueryBlockStarts = [0]
        self.mSbjctBlockStarts = [start]
        self.mNBlocks = 1

    def getBlocks(self):
        """return a list of aligned blocks."""
        return list(zip(self.mQueryBlockStarts,
                        self.mSbjctBlockStarts,
                        self.mBlockSizes))

    def __str__(self):
        return "\t".join(map(str, (
            self.mNMatches,
            self.mNMismatches,
            self.mNRepMatches,
            self.mNns,
            self.mQueryNGapsCounts,
            self.mQueryNGapsBases,
            self.mSbjctNGapsCounts,
            self.mSbjctNGapsBases,
            self.strand,
            self.mQueryId,
            self.mQueryLength,
            self.mQueryFrom,
            self.mQueryTo,
            self.mSbjctId,
            self.mSbjctLength,
            self.mSbjctFrom,
            self.mSbjctTo,
            self.mNBlocks,
            ",".join(map(str, self.mBlockSizes)) + ",",
            ",".join(map(str, self.mQueryBlockStarts)) + ",",
            ",".join(map(str, self.mSbjctBlockStarts)) + ",")))

    def getMapQuery2Target(self):
        """return a map between query to target.

        If the strand is "-", the coordinates for query are on
        the negative strand.
        """

        map_query2target = alignlib_lite.py_makeAlignmentBlocks()

        f = alignlib_lite.py_AlignmentFormatBlat(
            "%i\t%i\t%i\t%i\t%s\t%s\t%s\n" % (
                min(self.mQueryBlockStarts),
                max(self.mQueryBlockStarts),
                min(self.mSbjctBlockStarts),
                max(self.mSbjctBlockStarts),
                ",".join([str(x) for x in self.mQueryBlockStarts]) + ",",
                ",".join([str(x) for x in self.mSbjctBlockStarts]) + ",",
                ",".join([str(x) for x in self.mBlockSizes]) + ","))
        f.copy(map_query2target)

        return map_query2target

    def getMapTarget2Query(self):
        """return a map between target to query.

        If the strand is "-", the coordinates for query are on
        the negative strand.
        """

        map_target2query = alignlib_lite.py_makeAlignmentBlocks()

        f = alignlib_lite.py_AlignmentFormatBlat(
            "%i\t%i\t%i\t%i\t%s\t%s\t%s\n" % (
                min(self.mSbjctBlockStarts),
                max(self.mSbjctBlockStarts),
                min(self.mQueryBlockStarts),
                max(self.mQueryBlockStarts),
                ",".join([str(x) for x in self.mSbjctBlockStarts]) + ",",
                ",".join([str(x) for x in self.mQueryBlockStarts]) + ",",
                ",".join([str(x) for x in self.mBlockSizes]) + ","))
        f.copy(map_target2query)
        return map_target2query

    def copy(self):
        return copy.copy(self)

    def fromMap(self, map_query2target, use_strand=None):
        """return a map between query to target."""

        self.mNMatches = map_query2target.getNumAligned()
        f = str(alignlib_lite.py_AlignmentFormatBlat(map_query2target))

        self.mQueryFrom, self.mQueryTo, self.mSbjctFrom, self.mSbjctTo, \
            self.mQueryBlockStarts, self.mSbjctBlockStarts, self.mBlockSizes = f.split(
                "\t")

        if self.mBlockSizes:
            self.mBlockSizes = list(map(int, self.mBlockSizes[:-1].split(",")))
            self.mQueryBlockStarts = list(map(
                int, self.mQueryBlockStarts[:-1].split(",")))
            self.mSbjctBlockStarts = list(map(
                int, self.mSbjctBlockStarts[:-1].split(",")))
        else:
            self.mBlockSizes = []
            self.mQueryBlockStarts = []
            self.mSbjctBlockStarts = []

        self.mNBlocks = len(self.mBlockSizes)

        self.mQueryFrom, self.mQueryTo, self.mSbjctFrom, self.mSbjctTo = \
            list(map(int,
                     (self.mQueryFrom, self.mQueryTo,
                      self.mSbjctFrom, self.mSbjctTo)))

        # queryfrom and queryto are always forward strand coordinates
        if use_strand and self.strand == "-":
            self.mQueryFrom, self.mQueryTo = self.mQueryLength - \
                self.mQueryTo, self.mQueryLength - self.mQueryFrom

    def fromPair(self,
                 query_start, query_size, query_strand, query_seq,
                 target_start, target_size, target_strand, target_seq):
        '''fill from two aligned sequences.

        Note that sequences are case-sensitive.'''

        self.mQueryLength = query_size
        self.mSbjctLength = target_size

        map_query2target = alignlib_lite.py_makeAlignmentBlocks()

        assert len(query_seq) == len(target_seq)

        x, y = query_start, target_start
        nmatches, nmismatches = 0, 0
        for q, t in zip(query_seq, target_seq):
            tq, tt = q != "-", t != "-"
            if tq and tt:
                map_query2target.addPair(x, y)
                if q == t:
                    nmatches += 1
                else:
                    nmismatches += 1

            if tq:
                x += 1
            if tt:
                y += 1

        self.mNMatches, self.mNMismatches = nmatches, nmismatches
        self.strand = query_strand
        # the following call will set query_from, query_to for the forward strand
        # though block coordinates might be on the negative strand
        self.fromMap(map_query2target, use_strand=True)

        # if target is on negative strand, swop strands
        if target_strand == "-":
            # swap target strand - this will also swap the query strand
            self.switchTargetStrand()

    def iterator_introns(self):

        b = self.getBlocks()
        last_tend = b[0][1] + b[0][2]
        for qstart, tstart, size in b[1:]:
            yield (last_tend, tstart)
            last_tend = tstart + size
        raise StopIteration

    def iterator_exons(self):

        for qstart, tstart, size in self.getBlocks():
            yield tstart, tstart + size
        raise StopIteration

    def iterator_introns(self):

        b = self.getBlocks()
        last_tend = b[0][1] + b[0][2]
        for qstart, tstart, size in b[1:]:
            yield (last_tend, tstart)
            last_tend = tstart + size
        raise StopIteration

    def iterator_query_exons(self):

        for qstart, tstart, size in self.getBlocks():
            yield qstart, qstart + size
        raise StopIteration

    def iterator_sbjct_exons(self):

        for qstart, tstart, size in self.getBlocks():
            yield tstart, tstart + size
        raise StopIteration

    def iterator_query_introns(self):

        b = self.getBlocks()
        last_qend = b[0][0] + b[0][2]
        for qstart, tstart, size in b[1:]:
            yield (last_qend, qstart)
            last_qend = qstart + size
        raise StopIteration

    def getHeader(self):

        return HEADER

    def getHeaders(self):
        return ("match", "mismatch", "repeats", "Ns",
                "qGapCount", "qGapBases", "tGapCount", "tGapBases", "strand",
                "qName", "qSize", "qStart", "qEnd",
                "tName", "tSize", "tStart", "tEnd",
                "blockCount", "blockSizes",
                "qStarts", "tStarts")


@deprecated_class
class MatchPSLX(Match):

    def __init__(self):
        Match.__init__(self)
        self.mQuerySequence = []
        self.mSbjctSequence = []

    def __str__(self):
        return "\t".join((Match.__str__(self),
                          ",".join(self.mQuerySequence) + ",",
                          ",".join(self.mSbjctSequence) + ","))

    def fromTable(self, data):

        Match.fromTable(self, data)

        try:
            query_sequence, sbjct_sequence = data[21:23]

        except ValueError:
            raise ParsingError("parsing error", "\t".join(data))

        self.mQuerySequence = query_sequence[:-1].split(",")
        self.mSbjctSequence = sbjct_sequence[:-1].split(",")

    def getHeaders(self):
        return Match.getHeaders(self) + ("qSequence", "tSequence")

    def fromPSL(self, other, query_sequence, sbjct_sequence):
        """fill entry from a psl match.

        sequences are on forward strand starting at
        query_from and sbjct_from, respectively.
        """

        (self.mNMatches,
         self.mNMismatches,
         self.mNRepMatches,
         self.mNns,
         self.mQueryNGapsCounts,
         self.mQueryNGapsBases,
         self.mSbjctNGapsCounts,
         self.mSbjctNGapsBases,
         self.strand,
         self.mQueryId,
         self.mQueryLength,
         self.mQueryFrom,
         self.mQueryTo,
         self.mSbjctId,
         self.mSbjctLength,
         self.mSbjctFrom,
         self.mSbjctTo,
         self.mNBlocks,
         self.mBlockSizes,
         self.mQueryBlockStarts,
         self.mSbjctBlockStarts) = (other.mNMatches,
                                    other.mNMismatches,
                                    other.mNRepMatches,
                                    other.mNns,
                                    other.mQueryNGapsCounts,
                                    other.mQueryNGapsBases,
                                    other.mSbjctNGapsCounts,
                                    other.mSbjctNGapsBases,
                                    other.strand,
                                    other.mQueryId,
                                    other.mQueryLength,
                                    other.mQueryFrom,
                                    other.mQueryTo,
                                    other.mSbjctId,
                                    other.mSbjctLength,
                                    other.mSbjctFrom,
                                    other.mSbjctTo,
                                    other.mNBlocks,
                                    other.mBlockSizes,
                                    other.mQueryBlockStarts,
                                    other.mSbjctBlockStarts)

        if self.strand == "-":
            query_sequence = query_sequence.translate(str.maketrans("ACGTacgt", "TGCAtgca"))[::-1]

        self.mQuerySequence = []
        self.mSbjctSequence = []
        offset_query = min(self.mQueryBlockStarts)
        offset_sbjct = min(self.mSbjctBlockStarts)

        for start_query, start_sbjct, size in other.getBlocks():
            self.mQuerySequence.append(
                query_sequence[start_query - offset_query:start_query - offset_query + size])
            self.mSbjctSequence.append(
                sbjct_sequence[start_sbjct - offset_sbjct:start_sbjct - offset_sbjct + size])


def _iterate(infile):
    """iterator over psl output.

    The header is optional.
    """

    format = "ps3"

    while 1:
        line = infile.readline()
        if not line:
            raise StopIteration

        if line[0] == "#":
            continue

        if line.startswith("match"):
            continue

        if line.startswith("psLayout version 3"):
            format = "ps3"
            for x in range(4):
                infile.readline()
            continue

        match = Match()
        match.fromTable(line[:-1].split("\t"))
        yield match


@deprecated_class
class BlatIterator:

    def __init__(self, f, *args, **kwargs):
        self.mIterator = _iterate(f)

    def __iter__(self):
        return self

    def __next__(self):
        try:
            return next(self.mIterator)
        except StopIteration:
            return None

    def next(self):
        return self.__next__()


def iterator2(infile):
    """iterate over the contents of a psl file.
    """

    while 1:
        line = infile.readline()
        if not line:
            raise StopIteration
        if line[0] == "#":
            continue
        if line.startswith("match"):
            continue
        if line.startswith("psLayout version 3"):
            for x in range(4):
                infile.readline()
            continue
        match = Match()
        # match.fromTable( line[:-1].split() )
        yield match


def iterator(infile):
    """iterate over the contents of a psl file.
    """
    while 1:
        line = infile.readline()
        if not line:
            raise StopIteration
        if line[0] == "#":
            continue
        if line.startswith("match"):
            continue
        if line.startswith("psLayout version 3"):
            for x in range(4):
                infile.readline()
            continue
        match = Match()
        match.fromTable(line[:-1].split())
        yield match


def iterator_pslx(infile):
    """iterate over the contents of a pslx file.
    """
    while 1:
        line = infile.readline()
        if not line:
            raise StopIteration
        if line[0] == "#":
            continue
        if line.startswith("match"):
            continue
        if line.startswith("psLayout version 3"):
            for x in range(4):
                infile.readline()
            continue
        match = MatchPSLX()
        match.fromTable(line[:-1].split())
        yield match


def iterator_target_overlap(infile, merge_distance):
    '''iterate over psl formatted infile and return
    blocks of target overlapping alignments.'''

    last_sbjct_id = None
    start = None
    end = None
    matches = []
    processed_contigs = set()

    while 1:

        match = next(infile)

        if match is None:
            break

        if match.mSbjctId != last_sbjct_id or match.mSbjctFrom >= (end + merge_distance):
            if last_sbjct_id:
                yield matches
            matches = []
            if last_sbjct_id != match.mSbjctId and match.mSbjctId in processed_contigs:
                raise ValueError("input not sorted by target (contig,start): already encountered %s\n%s" %
                                 (match.mSbjctId, str(match)))

            processed_contigs.add(last_sbjct_id)

            last_sbjct_id = match.mSbjctId
            start = match.mSbjctFrom
            end = match.mSbjctTo

        if match.mSbjctFrom < start:
            raise ValueError("input not sorted by target (contig,start): %i < %i\n%s" %
                             (match.mSbjctFrom, start, str(match)))

        end = max(match.mSbjctTo, end)
        matches.append(match)

    if last_sbjct_id:
        yield matches


def iterator_query_overlap(infile, merge_distance):
    '''iterate over psl formatted infile and return
    blocks of target overlapping alignments.'''

    last_query_id = None
    start, end = None, None
    matches = []
    processed_contigs = set()

    while 1:

        match = next(infile)

        if match is None:
            break

        if match.mQueryId != last_query_id or match.mQueryFrom >= (end + merge_distance):
            if last_query_id:
                yield matches
            matches = []

            if last_query_id != match.mQueryId and match.mQueryId in processed_contigs:
                raise ValueError("input not sorted by query (contig,start): already encountered %s\n%s" %
                                 (match.mQueryId, str(match)))

            processed_contigs.add(last_query_id)

            last_query_id = match.mQueryId
            start, end = match.mQueryFrom, match.mQueryTo

        if match.mQueryFrom < start:
            raise ValueError("input not sorted by query (contig,start): %i < %i\n%s" %
                             (match.mQueryFrom, start, str(match)))

        end = max(match.mQueryTo, end)
        matches.append(match)

    if last_query_id:
        yield matches


def iterator_test(infile, report_step=100000):
    '''only output parseable lines from infile.'''

    ninput, noutput, nerrors = 0, 0, 0

    while 1:
        try:
            x = next(infile)
        except ParsingError as msg:
            nerrors += 1
            ninput += 1
            E.warn(str(msg))
            continue
        except StopIteration:
            break

        if not x:
            break

        ninput += 1

        if ninput % report_step == 0:
            E.info("progress: ninput=%i, noutput=%i" % (ninput, noutput))

        yield x
        noutput += 1

    E.info("iterator_test: ninput=%i, noutput=%i, nerrors=%i" %
           (ninput, noutput, nerrors))


def iterator_per_query(iterator_psl):
    """iterate over the contents of a psl file per query
    """

    data = collections.defaultdict(list)

    for match in iterator_psl:
        data[match.mQueryId].append(match)

    for x in list(data.values()):
        yield x

    raise StopIteration

FIELDS = ("matches",
          "misMatches",
          "repMatches",
          "nCount",
          "qNumInsert",
          "qBaseInsert",
          "tNumInsert",
          "tBaseInsert",
          "strand",
          "qName",
          "qSize",
          "qStart",
          "qEnd",
          "tName",
          "tSize",
          "tStart",
          "tEnd",
          "blockCount",
          "blockSizes",
          "qStarts",
          "tStarts")


def addAlignments(matches, shift=0, by_query=False):
    """building a genome to query alignment for all matches

    The genome alignment is shifted by *shift*.
    """

    if by_query:
        for match in matches:
            if not hasattr(match, "mMapQuery2Target"):
                map_query2target = match.getMapQuery2Target()
                if shift:
                    map_query2target.moveAlignment(shift, 0)
                match.mMapQuery2Target = map_query2target
    else:
        for match in matches:
            if not hasattr(match, "mMapTarget2Query"):
                map_target2query = match.getMapTarget2Query()
                if shift:
                    map_target2query.moveAlignment(shift, 0)
                match.mMapTarget2Query = map_target2query


def getComponents(matches, max_distance=0, min_overlap=0, by_query=False):
    """return overlapping matches.

    max_distance
       allow reads to be joined if they are # residues apart.
       Adjacent reads are 1 residue apart, overlapping reads are 0 residues
       apart
    min_overlap
       require at least # residues to be overlapping

    """

    addAlignments(matches, by_query=by_query)

    components = Components.IComponents()

    for x in range(0, len(matches)):
        components.add(x, x)

    if min_overlap > 0 and max_distance > 0:
        raise ValueError(
            "both min_overlap (%i) and max_distance (%i) > 0" % (min_overlap, max_distance))

    if by_query:
        if min_overlap > 0:
            f = lambda x, y: alignlib_lite.py_getAlignmentOverlap(
                matches[x].mMapQuery2Target,
                matches[y].mMapQuery2Target,
                alignlib_lite.py_RR) >= min_overlap
        else:
            f = lambda x, y: alignlib_lite.py_getAlignmentShortestDistance(
                matches[x].mMapQuery2Target,
                matches[y].mMapQuery2Target,
                alignlib_lite.py_RR) <= max_distance
    else:
        if min_overlap > 0:
            f = lambda x, y: alignlib_lite.py_getAlignmentOverlap(
                matches[x].mMapTarget2Query,
                matches[y].mMapTarget2Query,
                alignlib_lite.py_RR) >= min_overlap
        else:
            f = lambda x, y: alignlib_lite.py_getAlignmentShortestDistance(
                matches[x].mMapTarget2Query,
                matches[y].mMapTarget2Query,
                alignlib_lite.py_RR) <= max_distance

    for x in range(len(matches)):
        for y in range(0, x):
            if f(x, y):
                components.add(x, y)

    return components.getComponents()
